4

许久之前学习WinForm的时候开始解除的C#,之后便搁了好长时间。最近在学Unity的时候又重拾了C#。发现以前学过的东西也都忘了差不多了。特别在Unity中会经常用到yield关键字和IEnumerator接口来做一些延时循环的操作。作为重拾C#第一步,先来复习和总结一下C#中的IEnumerableIEnumerator接口。

背景

在很多编程场景中,我们需要去遍历(Iterate)一个集合。为了让这个过程简化,很多高级语言都采用遍历语句来进行操作,例如for...in... 或者foreach()。例如在C#中,ArrayList的实例可以在foreach()中得到遍历。当然除了语言原生的一些集合类外不是所有的类的实例都可以放在遍历语句中进行操作的。要想让一个类的实例能够在遍历语句中得到遍历,就必须按照语言的规定让类实现某些接口或者属性。

原生ArrayList

在我们开始正式进入正题前,我们先看一下C#原生的ArrayList是怎么样工作的。

using System.IO;
using System;
using System.Collections;

class Test
{
    static void Main()
    {
        ArrayList array = new ArrayList();
        array.Add(1);
        array.Add(2);
        array.Add("3");
        array.Add(4);
        foreach(object i in array){
            Console.WriteLine(i);
        }
    }
}

很简单是吧,那我们下面就开始阐述IEnumerableIEnumerator接口。并实现一个自己版本的ArrayList

开始实现自己的ArrayList

首先我们来认识一下我们今天的主角,IEnumerable, IEnumerable<T>, IEnumerator, IEnumberator<T>,长的真的很像呢。首先我们一眼就可以看出后面带的就是有类型的(C#里叫做generic)。generic版本和non-generic版本稍有些不同。作为开始我们先实现non-generic版吧。那就选定了我们的男一和女一,IEnumerable, IEnumerator。瞅一眼它们的简历吧。官方文档是这么写的:

IEnumerable is the base interface for all non-generic collections that can be enumerated. For the generic version of this interface see System.Collections.Generic.IEnumerable. IEnumerable contains a single method, GetEnumerator, which returns an IEnumerator. IEnumerator provides the ability to iterate through the collection by exposing a Current property and MoveNext and Reset methods.

IEnumerable是那些可以被遍历的集合中所需要实现的基础接口,IEnumerable有一个方法GetEnumerator(),这个方法发回一个IEnumerator类型,IEnumerator包含一个Current属性和MoveNextReset方法,通过这些属性和方法就可以遍历这个集合了。所以我们自己的ArrayList应该这么实现:

public class MyArrayList: IEnumerable
{

    //some code
    
    public IEnumerator GetEnumerator()
    {
       //some code
       //return new MyEnumerator(...);
    }
}

可以看出GetEnumerator返回一个IEnumerator类型,所以我们就必须要去实现自己的IEnumerator类:

public class MyEnumerator:IEnumerator
{
    
    public bool MoveNext()
    {
        //some code
    }
    
    public void Reset()
    {
        //some code
    }
    
    public object Current
    {
        get
        {
            // some code
        }
    }
}

知道了基本结构,就可以扩展出我们想要的ArrayList结构了,完整的代码如下:

using System.IO;
using System;
using System.Collections;


public class MyArrayList
{
    
    object[] data;
    int currentIndex;
    
    public MyArrayList(int length)
    {
        this.data = new object[length];
        currentIndex = 0;
    }
    
    public void Add(object s)
    {
        data[currentIndex++] = s;
    }
    
    public IEnumerator GetEnumerator()
    {
        return new MyEnumerator(data);
    }
}

public class MyEnumerator:IEnumerator
{
    
    private object[] _data;
    private int position = -1;
    
    public MyEnumerator(object[] data)
    {
        _data = data;
    }
    
    public bool MoveNext()
    {
        position++;
        return (position < _data.Length);
    }
    
    public void Reset()
    {
        position = -1;
    }
    
    object IEnumerator.Current
    {
        get
        {
            return Current;
        }
    }
    
    public object Current
    {
        get
        {
            return _data[position];
        }
    }
}

class Test
{
    static void Main()
    {
        MyArrayList array = new MyArrayList(10);
        array.Add("Jack");
        array.Add("Tom");
        foreach(object i in array)
        {
            Console.WriteLine(i);
        }
    }
}

这样一个简单的ArrayList就实现了。还有一点要注意的就是IEnumerable接口不是必须要实现的,但是要想能遍历,必须要实现GetEnumerator()方法。但是实现IEnumerableIEnumberator接口是个好习惯。

IEnumerable和IEnumerator的Generic版本

对于IEnumerable<T>IEnumerator<T>的实现,稍有些不同,如果我们只是把上面代码中的IEnumerableIEnumerator换成对应的Generic接口的话:

public class MyArrayList<T>: IEnumerable<T>
{

    //some code
    
    public IEnumerator<T> GetEnumerator()
    {
       //some code
       //return new MyEnumerator<T>(...);
    }
}
public class MyEnumerator<T>:IEnumerator<T>
{
    
    public bool MoveNext()
    {
        //some code
    }
    
    public void Reset()
    {
        //some code
    }
    
    public T Current
    {
        get
        {
            // some code
        }
    }
}

这样编译器会报三个错误:

1.MyEnumerator<T> does not implement interface member System.IDisposable.Dispose()
2.MyArrayList<T> does not implement interface member System.Collections.IEnumerable.GetEnumerator() and the best implementing candidate MyArrayList<T>.GetEnumerator() return type System.Collections.Generic.IEnumerator<T> does not match interface member return type System.Collections.IEnumerator
3.MyEnumerator<T> does not implement interface member System.Collections.IEnumerator.Current.get and the best implement ing candidate MyEnumerator<T>.Current.get return type T does not match interface member return type object

第一个错误告诉我们IEnumerable<T>要实现Dispose()方法,第二个,第三个错误要我们实现IEnumerable.GetEnumerator()IEnumerator.Current属性。完整代码如下:

using System.IO;
using System;
using System.Collections;
using System.Collections.Generic;


public class MyArrayList<T>: IEnumerable<T>
{
    
    T[] data;
    int currentIndex;
    
    public MyArrayList(int length)
    {
        this.data = new T[length];
        currentIndex = 0;
    }
    
    public void Add(T s)
    {
        data[currentIndex++] = s;
    }
    
    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
    
    public IEnumerator<T> GetEnumerator()
    {
        return new MyEnumerator<T>(data);
    }
}

public class MyEnumerator<T>:IEnumerator<T>
{
    
    private T[] _data;
    private int position = -1;
    
    public MyEnumerator(T[] data)
    {
        _data = data;
    }
    
    public bool MoveNext()
    {
        position++;
        return (position < _data.Length);
    }
    
    public void Reset()
    {
        position = -1;
    }
    
    public void Dispose()
    {
        //Dispose the resource
    }
    
    object IEnumerator.Current
    {
        get
        {
            return Current;    
        }
    }
    
    public T Current
    {
        get
        {
            return _data[position];
        }
    }
}

public class Test
{
    static void Main()
    {
        MyArrayList<string> array = new MyArrayList<string>(10);
        array.Add("Jack");
        array.Add("Tom");
        foreach(string str in array)
        {
            Console.WriteLine(str);
        }
    }
}

yield关键字

用上面的方法来实现一个可以遍历的类多少觉得有些麻烦,要多实现一个IEnumerator类。而且Generic版本要多实现几个方法和属性。yield关键字可以帮我们简化上述的过程。yield用于生成一个遍历类型,包含yield的方法的返回值类型必须是IEnumerable,IEnumerator,IEnumerable<T>,IEnumerator<T>其中一种。yield的一般形式是yield return <expression>。例如:

public class PowersOf2
{
    static void Main()
    {
        // Display powers of 2 up to the exponent of 8:
        foreach (int i in Power(2, 8))
        {
            Console.Write("{0} ", i);
        }
    }

    public static System.Collections.Generic.IEnumerable<int> Power(int number, int exponent)
    {
        int result = 1;

        for (int i = 0; i < exponent; i++)
        {
            result = result * number;
            yield return result;
        }
    }

    // Output: 2 4 8 16 32 64 128 256
}

调用上面的Power方法,不会执行函数的主体,而是返回一个IEnumerable<int>对象,在foreach中,调用MoveNext来进行遍历,这时函数开始执行,指导碰到yield,后面返回的对象就是Current属性的值。下次调用MoveNext的时候,会从上个yield的地方继续往后执行。这样知道函数结束,遍历也就结束了。知道这个特性后我们就能简化上面的代码了:

using System.IO;
using System;
using System.Collections;
using System.Collections.Generic;


public class MyArrayList<T>: IEnumerable<T>
{
    
    T[] data;
    int currentIndex;
    
    public MyArrayList(int length)
    {
        this.data = new T[length];
        currentIndex = 0;
    }
    
    public void Add(T s)
    {
        data[currentIndex++] = s;
    }
    
    IEnumerator IEnumerable.GetEnumerator(){
        return this.GetEnumerator();
    }
    
    public IEnumerator<T> GetEnumerator()
    {
        for(int i = 0; i < data.Length; i++)
        {
            yield return data[i];
        }
    }
}

public class Test
{
    static void Main()
    {
        MyArrayList<string> array = new MyArrayList<string>(10);
        array.Add("Jack");
        array.Add("Tom");
        foreach(string str in array)
        {
            Console.WriteLine(str);
        }
    }
}

总结

对于遍历的实现,每个语言都有自己不同的实现,但却有很大的相似处,了解一个语言的实现也有助于对其他语言实现的理解。结下来会写一下javascript中对于遍历的实现。


leeif
458 声望41 粉丝

前日本打工人,bytedancer,现在微软搬砖,沉迷chatgpt中。