枚举和迭代器

Posted mingjie-c

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了枚举和迭代器相关的知识,希望对你有一定的参考价值。

Enumeration 和 Iterators

 

先来说一下 Enumerator 枚举器

枚举器是一个只读的,作用于一序列值的,只能向前的游标。

枚举器是一个实现了下列任意接口的对象:

  • System.Collections.IEnumerator

  • System.Collections.Generic.IEnumerator<T>

从技术上来说,任何一个含有名为MoveNext 方法和名为 Current 的属性对象,都会被当作枚举器来对待。

foreach 语句会迭代可枚举的对象(enumerable object)。可枚举的对象是一序列值的逻辑表示。它本身不是游标,它是一个可以基于本身产生游标的对象。

注意:枚举器和枚举类型没什么关系,下面是枚举类型的解释。

  • 枚举类型定义了一组“符号名称 / 值” 配对

  • 枚举是值类型,从System.Enum 派生,后者从System.ValueType 派生。

  • 枚举不能像值类型那样定义方法、属性、事件,但可利用C#的扩展方法功能模拟向枚举类型添加方法。

    internal static class FileAttributesExtensionMethods {
       public static FileAttributes Set(this FileAttributes flags, FileAttributes setFlags) {
      return flags | setFlags;
      }
       public static FileAttributes Clear(this FileAttributes flags,
       FileAttributes clearFlags) {
      return flags & ~clearFlags;
      }
       public static void ForEach(this FileAttributes flags,
       Action<FileAttributes> processFlag) {
      if (processFlag == null) throw new ArgumentNullException("processFlag");
           for (UInt32 bit = 1; bit != 0; bit <<= 1) {
               UInt32 temp = ((UInt32)flags) & bit;
               if (temp != 0) processFlag((FileAttributes)temp);
          }
      }
    }

    下面是调用方式,看起来就像是枚举有了方法一样。

    FileAttributes fa = FileAttributes.System;
    fa = fa.Set(FileAttributes.ReadOnly);
    fa = fa.Clear(FileAttributes.System);
    fa.ForEach(f => Console.WriteLine(f))

     

下面介绍可枚举对象 enumerable object

一个可枚举对象可以是(下列任意一个):

  • 实现了IEnumerable 或者 IEnumerable<T> 的对象。

  • 有一个名为GetIEnumerator 的方法,并且该方法返回一个枚举器(enumerator)。

IEnumerator 和 IEnumerable 是定义在System.Collections 命名空间下的。

IEnumerator<T> 和 IEnumerable<T> 是定义在System.Collections.Generic 命名空间下的。

 

枚举模式 enumeration pattern

class Enumerator 
{
public IteratorVariableType Current {get {...}}
public bool MoveNext() {...}
}
class Enumerable
{
public Enumerator GetEnumerator() {...}
}

看一个例子:

foreach (char c in "beer")
Console.WriteLine(c);

using (var enumerator = "beer".GetEnumerator())
{
while(enumerator.MoveNext())
{
var element = enumerator.Current;
Console.WriteLine(element);
}
}

注意:如果枚举器(enumerator)实现了IDisposable 接口,那么foreach 语句就会像using 语句那样,隐式的dispose 掉这个 enumerator对象。

 

集合初始化器

你可以只用一步就把可枚举对象进行实例化并且填充里面的元素:

using System.Collections.Generic:
...
List<int> list = new List<int> {1,2,4};

但是C# 编译器会把它翻译成:

using System.Collections.Generic;
...
List<int> list = new List<int>();
list.add(1);
list.add(2);
list.add(4);

凡是含有索引器的集合都可以这样写。

 

迭代器 Iterators

迭代器是含有一个或多个yield 语句的方法,属性或索引器。

迭代器必须返回下面四个接口中的一个(否则编译器会报错):

  • //Enumerable interfoces

    • System.Collections.IEnumerable<T>

    • System.Collections.Generic.IEnumerable<T>

  • //Enumerator interfaces

    • System.Collections.IEnumerator

    • System.Collections.IEnumerator<T>

根据迭代器返回的是 enumerable 接口还是enumerator 接口,迭代器会有不同的语义。

foreach 语句是枚举器(enumerate)的消费者,而迭代器是枚举器的生产者。看一个例子:

using System;
using System.Collections.Generic;
calss Test
{
static void Main()
{
foreach(int fib in Fibs(6))
Console.Write(fib + " ");
}

static IEnumerable<int> Fibs(int fibCount)
{
for(int i=0, prevFib =1, curFib=1; i<fibCount;i++)
{
yield return prevFib;
int newFib = prevFib + curFib;
prevFib = curFib;
curFib = newFib;
}
}
}

迭代器的原理:

  • 编译器把迭代方法转换成私有的,实现了IEnumerable<T> 和/或 IEnumerator<T> 的类

  • 迭代器块内部的逻辑被反转并且被切分到编译器生成的枚举器类里面的MoveNext 方法和 Current 属性里。

  • 这意味着当你调用迭代器方法时,你所做的实际就是编译器生成的类进行实例化。

  • 你写的代码仅会在对结果序列进行枚举的时候才会运行,例如使用foreach 语句。

 

yield break

yield break 语句表示迭代器块会提前退出,不再返回更多的元素。

static IEnumerable<string> Foo (bool breakEarly)
{
yield return "One";
yield return "Two";
if(breakeEarly)
yield break;
yield return "Three"; //非法
}

return 语句在迭代器块里面是非法的,你必须使用yield break 代替。

 

迭代器和 try/catch/finally 块

yield return 语句不可以出现含有在 catch 子句的try 块里面:

IEnumerable<string> Foo()
{
try {yield return "One";} //非法的
catch {...} //在catch 也是非法的
}
//yield return 也不能出现在catch 或者finally 块里面。

但是 yield return 可以出现在只含有finally 块的try 块里面:

IEnumerable<string> Foo()
{
try {yield return "One";} //合法
finally {...}
}
  • 当消费的枚举器到达序列终点时或被disposed 的时候,finally 块里面的代码会执行。

  • 如果你提前进行了break,那么foreach 语句也会dispose 掉枚举器,所以用起来和安全。

  • 当显示使用枚举器的时候,通常会范这样一个错误:没有dispose 掉枚举器就不再用它了,这就绕开了finally 块。针对这种情况,你可以使用using 语句来规避风险:

    string firstElement =null;
    var sequence = Foo();
    using (var enumerator = sequence.GetEnumerator())
    if(enumerator.MoveNext())
    firstElement = enumerator.Current;
  •  

以上是关于枚举和迭代器的主要内容,如果未能解决你的问题,请参考以下文章

枚举和迭代器

C#知识点-枚举器和迭代器

枚举器和迭代器

设计模式 - 适配器模式(adapter pattern) 枚举器和迭代器 具体解释

枚举器、迭代器、IEnumerable - 有点困惑

csharp 缓存枚举器,确保源枚举器只迭代一次