枚举器和迭代器

  1. 枚举器、可枚举类型
    • 数组可以按需提供一个叫做枚举器的对象
    • 枚举器可以依次返回请求的数组中的元素
    • 获取一个对象枚举器的方法是调用GetEnumerator方法,实现该方法的类型称为可枚举类型
    • foreach结构设计用来和可枚举类型一起使用,只要遍历对象是可枚举类型,就会执行:
      • 调用GetEnumerator方法获取对象的枚举器
      • 从枚举器中请求每一项并且把它作为迭代变量,代码可以读取该变量但是不可改变
  2. IEnumerator接口
    • 实现了IEnumerator接口的枚举器包含三个函数成员:currentMoveNext, Reset
    • Current返回当前位置的属性
      • 只读
      • 返回object类型的引用,所以可以返回任何类型
    • MoveNext把枚举器前进到集合中的下一项,并返回新位置是否有效
      • 初始位置在第一项之前,所以使用Current前必须至少调用一次MoveNext
    • Reset重置枚举器的位置
    • 利用枚举器模仿foreach循环遍历集合中的项
    • int[] MyArray = {1, 2, 3, 4, 5};
      IEnumerator ie = MyArray.GetEnumerator();
      while (ie.MoveNext()) {
          System.Console.WriteLine((int)ie.Current);
      }
      
    • 编写foreach时,C#编译器会生成与上述代码逻辑十分类似CIL形式的代码
  3. IEnumerable接口
    • IEnumerable接口只有一个成员:GetEnumerator方法,返回对象的枚举器
  4. 使用IEnumerator接口和IEnumerable接口
    • 可枚举类型实现IEnumerable接口,实现方法GetEnumerator方法,返回值为另一个枚举器对象
    • 枚举器对象实现IEnumerator接口,实现方法MoveNextReset以及属性Current
  5. 泛型枚举接口
    • 非泛型接口形式
      • IEnumerable接口的GetEnumerator方法返回实现IEnumerator枚举器类的实例
      • 实现IEnumerator的类实现了Current属性,它返回object引用,使用需要强转为实际类型
      • 实现不是类型安全的,在手动强转时可能会发生异常
    • 泛型接口
      • IEnumerable<T>接口的GetEnumerator方法返回IEnumerator<T>的枚举器类的实例
      • 实现IEnumerator<T>的类实现的Current类型返回实际类型的的对象
      • 返回实际类型的引用,是类型安全的,如果要自己创建可枚举类,应该实现这些泛型接口
    • 注意
      • 泛型接口也实现了非泛型接口,目的貌似是强制向下兼容,因为在C#2.0版本以前没有泛型
      • 因此需要提供泛型与非泛型两个版本的方法实现
      • 由于泛型接口和非泛型接口的方法/属性同名同参数列表但是返回类型不同(CurrentGetGetEnumerator
      • 所以需要使用显式接口成员实现,返回类型一致的如MoveNextReset只需要提供一份实现即可
      • 另外IEnumerator<T>泛型版本还实现了IDisposable接口,所以还需要手动实现Dispose方法释放资源
  6. 迭代器
    • C#从2.0版本开始提供了更简单的创建枚举器和可枚举类型的方法。实际上,编译器将为我们创建它们,这种结构叫做迭代器
    • 迭代器块
      • 有一个或多个yield语句的代码块
      • 迭代器块与其他语句块不同,其他块包含的语句被当作是命令式的。也就是说,先执行代码的第一个语句,然后执行后面的语句,最后控制离开块
      • 另一方面,迭代器块不是需要在同一时间执行的一串命令式命令,而是描述了希望编译器为我们创建的枚举器类的行为,迭代器块中的代码描述了如何枚举元素
      • 两个特殊语句
        • yield return语句指定了序列中返回的下一项
        • yield break语句指定在序列中没用其他项
    • 使用迭代器来创建枚举器和可枚举类型
      • class Test {
            int[] arr = {1, 2, 3, 4, 5};
            public IEnumerator<int> GetEnumerator() {
                return TestEnumerator();
            }
            IEnumerator<int> TestEnumerator() {
                for (int i = 0; i < arr.Length; i++) {
                    yield return arr[i];
                }
            }
        }
        
      • 不需要实现IEnumerator接口,TestEnumerator返回值即为枚举器
    • 使用迭代器创建可枚举类型
      • class Test {
            int[] arr = {1, 2, 3, 4, 5};
            public IEnumerator<int> GetEnumerator() {
                return TestEnumerator().GetEnumerator();
            }
            IEnumerable<int> TestEnumerator() {
                for (int i = 0; i < arr.Length; i++) {
                    yield return arr[i];
                }
            }
        }
        
      • 不需要实现IEnumerable接口,TestEnumerator返回值即为可枚举类型,同时Test类本身也是可枚举类型
      • 在使用foreach遍历时可以使用类对象,也可以使用类枚举器方法TestEnumerator()
  7. 常见迭代器模式
    • 实现返回枚举器的迭代器时,必须通过实现GetEnumerator来让类可枚举,它返回由迭代器返回的枚举器
    • 如果实现迭代器返回可枚举类型
      • 可以实现GetEnumerator让类可枚举,让它调用迭代器方法以获取自动生成的实现IEnumerable的类实例,然后可枚举类型对象返回由GetEnumerator创建的枚举器
      • 也可以不实现GetEnumerator,此时类本身不可枚举,但迭代器返回的是可枚举类,可以直接调用迭代器方法
  8. 产生多个可枚举类型
    • 同一个类可以产生多个可枚举类型,只需要实现多个迭代器方法,这些方法返回可枚举类型,然后通过迭代器去获得可枚举类型,可以不实现GetEnumerator
    • 同理,一个类可以产生多个枚举器,但由于让类可枚举必须实现GetEnumerator方法,具体返回哪个迭代器的枚举器,可以通过一个布尔变量来进行选择
  9. 将迭代器作为属性
    • 使用类似迭代器方法,只不过把迭代器块作为属性的get访问器来声明
  10. 注意事项
    • 迭代器需要System.Collections.Generic命名空间
    • 使用迭代器时会由编译器产生枚举器,但这个枚举器中没有实现Reset方法,调用时会抛出`System.NotSupportedException异常
  11. 迭代器实质
    • 由编译器生成的枚举器类是包含四个状态的状态机
      • Before:首次调用MoveNext的初始状态
      • Running:调用MoveNext后进入这个状态。在这个状态中,枚举器检测并设置下一项的位置。在遇到yield returnyield break或在迭代器体结束时,退出状态
      • Suspended:状态机等待下次调用MoveNext的状态
      • After:没有更多项可以枚举
    • 如果状态机在Before或Suspended状态时调用了MoveNext方法,就转到Running状态。
    • Running状态结束时,如果有更多项,状态机转入Suspended状态,否则转入并保持在After状态
    • 状态机