详解C#迭代器

  一、迭代器(Iterator)通过持有迭代状态可以获取当前迭代元素并且识别下一个需要迭代的元素,从而可以遍历集合中每一个元素而不用了解集合的具体实现方式;

  实现迭代器功能的方法被称为迭代器方法,迭代器方法的返回值类型可以是以下4种接口类型中任意一种:位于命名空间System.Collections中的IEnumerable、IEnumerator和位于命名空间System.Collections.Generic中的IEnumerable<T>、IEnumerator<T>,其中接口IEnumerable和IEnumerator代码:

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}
public interface IEnumerator
{
    object Current { get; }

    bool MoveNext();
    void Reset();
}

  ※对应泛型接口IEnumerable<T>和IEnumerator<T>代码:

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
    T Current { get; }
}

  ※接口IEnumerable中声明了获取IEnumerator类型对象的方法GetEnumerator(),接口IEnumerator中通过属性Current和方法MoveNext()实现迭代器功能;自定义类型可以通过继承接口IEnumerable拥有迭代器功能,而接口IEnumerator为迭代器功能提供具体实现,设计二者符合单一职责原则,也因此可以对同一个对象同时进行多次迭代;

  ※get访问器也可以声明为迭代器方法,但事件、构造函数和析构函数不可以声明为迭代器方法;迭代器方法不能包含引用参数;

  二、迭代器方法的内部使用状态机实现,但可以使用yield关键字快速实现迭代器方法,使用yield return语句添加需要迭代的元素,在首次迭代时,会一直执行到第一个yield return语句并保存当前迭代状态,在接下来的每次迭代过程中都会从暂停的位置继续执行到下一个yield return语句并保存迭代状态(MoveNext()方法返回true并将当前迭代的值赋值给Current),直到到达迭代器的结尾(MoveNext()方法返回false)完成本次迭代;也可以在迭代过程中使用yield break语句立刻结束本次迭代:

IEnumerable<int> MyIterator()
{
    yield return 10;
    yield return 20;
}

  ※其中yield return语句的返回值需要可以隐式转换为迭代器方法返回值IEnumerable<T>中类型参数的类型;

  ※这是编译器为我们准备的一种语法糖,编译器会将其转换为使用状态机实现的实现了接口IEnumerable<T>的嵌套类,查看迭代器方法的IL代码,可以看到编译器生成的状态机嵌套类:

  三、对于实现了接口IEnumerable或IEnumerable<T>的类型的对象,可以使用foreach对其进行遍历:

foreach (int item in MyIterator())
{
    //do…其中item为int类型,值分别为10和20
}

  ※其中item实际上是类型中GetEnumerator()方法返回值类型中的Current属性,因此其类型即该属性的类型,通常可以使用var表示,由编译器进行推断,例如对哈希表进行遍历时:

foreach (var item in myHashTable)
{
    //do…其中item为DictionaryEntry类型,item.Key and item.Value
}

  ※由于属性Current只有get访问器,因此不能在foreach循环中对item进行赋值(=运算符操作)操作,但可以对其对象中的成员进行修改和调用,详见;

  ※在遍历内置集合的对象时,如果集合长度发生改变,则极有可能紊乱迭代过程,因此在迭代这些集合对象时不能进行任何修改集合长度的操作,否则会抛出异常InvalidOperationException;

  四、除使用foreach外,还可以手动进行迭代,对于实现了泛型接口的类型的对象在手动迭代时还需要使用using语句以在其迭代完成后调用Dispose()方法:

using (IEnumerator<int> enumerator = MyIterator().GetEnumerator())

{
    //此时myEnmerator.Current为其类型的默认值
    while (enumerator.MoveNext())
    {
        //do…其中可以通过enumerator.Current访问迭代器的值
    }
}    

  1.对于只实现了接口IEnumerator或IEnumerator<T>的类型的对象,只能手动进行迭代:

IEnumerator<int> MyIterator()
{
    yield return 1;
    yield return 2;
}
using (IEnumerator<int> enumerator = MyIterator())
{
    while (enumerator.MoveNext())
    {
        //do…
    }
}

  2.可以通过实现接口IEnumerable或IEnumerable<T>来自定义迭代器类型,此时不必手动实现IEnumerator或IEnumerator<T>接口,编译器会自动实现该接口中的Current属性、MoveNext()、Reset()和Dispose()方法:

public class MyIterator : IEnumerable<int>
{
    public IEnumerator<int> GetEnumerator()
    {
        yield return 10;
        yield return 20;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        Console.WriteLine("Non-Generic GetEnumerator");
        return GetEnumerator(); //注意这里不是yield return
        //或者手动进行迭代:
        //using (IEnumerator<int> iterator = this.GetEnumerator())
        //{
        //while (iterator.MoveNext())
        //{
        //yield return iterator.Current;
        //}
        //}
    }
}    

  ※此时,在使用foreach遍历时,如果使用var,会由编译器自动推断item为int类型,并调用泛型接口的实现,如果将item的类型指定为object类型,则依然会调用泛型接口的实现,但会产生装箱操作:

foreach (var item in new MyIterator()) //或显式指定为int:int item
{
    Console.WriteLine(item); //10 20
}

  ※此时,在使用foreach遍历时,如果将其转换为IEnumerable类型的对象,则会调用非泛型接口的实现,编译器将自动推断item为object类型,并产生装箱操作:

foreach (var item in (IEnumerable)new MyIterator()) //或显式指定为object:object item
{
    Console.WriteLine(item); //Non-Generic GetEnumerator 10 20
}

  3.直接调用迭代器方法或迭代器类型中自动生成的Reset()方法时会抛出异常NotSupportedException,若要从头开始重新迭代,必须获取新的迭代器,或在迭代器类型中手动实现Reset()方法;

 


如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的认可是我写作的最大动力!

作者:Minotauros
出处:https://www.cnblogs.com/minotauros/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

posted @ 2019-02-26 18:05 Minotauros 阅读(...) 评论(...) 编辑 收藏