·您现在的位置: 云翼网络 >> 文章中心 >> 网站建设 >> 网站建设开发 >> ASP.NET网站开发 >> .NetC#5.0规范:迭代器

.NetC#5.0规范:迭代器

作者:佚名      ASP.NET网站开发编辑:admin      更新时间:2022-07-23

本文内容

  • 枚举器 enumerator 接口 - IEnumerator
  • 可枚举 enumerable 接口 - IEnumerable
  • 产生类型 yield type
  • 枚举器 enumerator 对象 
  • 可枚举 enumerable 对象 
  • 示例
  • 参考资料

本文只是 C# 5.0 规范中的内容,稍作调整,主要是下载 Demo 看看,这玩意用法挺多。

使用迭代器块实现的函数成员称为迭代器(iterator)。(所谓函数成员,包括方法、属性、事件、索引器、用户定义运算符、实例构造函数、静态构造函数和析构函数。)

只要相应函数成员的返回类型是枚举器接口 enumerator 或可枚举接口 enumerable 之一,迭代器块就可用作该函数成员的函数体。如下“音乐标题”类所示,有三个函数,有返回 IEnumerator 的,还有返回 IEnumerable 的,其中,返回 Reverse 函数可以反序迭代一个集合,而 Subset 函数可以迭代一个集合的子集:

public class MusicTitles
{
    string[] names = { "Tubular Bells", "Hergest Ridge", "Ommadawn", "Platinum" };
 
    public IEnumerator GetEnumerator()
    {
        for (int i = 0; i < 4; i++)
        {
            yield return names[i];
        }
    }
 
    public IEnumerable Reverse()
    {
        for (int i = 3; i >= 0; i--)
        {
            yield return names[i];
        }
    }
 
    public IEnumerable Subset(int index, int length)
    {
        for (int i = index; i < index + length; i++)
        {
            yield return names[i];
        }
    }
}

可以用如下方式迭代:

foreach (string title in titles)
{
    Console.WriteLine(title);
}
 
foreach (string title in titles.Reverse())
{
    Console.WriteLine(title);
}
 
foreach (string title in titles.Subset(2, 2))
{
    Console.WriteLine(title);
}

迭代器块可以是 method-body、Operator-body 或 accessor-body,而不能将事件、实例构造函数、静态构造函数和析构函数作为迭代器来实现。

当使用迭代器块实现函数成员时,为该函数成员的形参列表指定任何 ref 或 out 形参将产生编译时错误。

下载 Demo

枚举器 enumerator 接口 - IEnumerator


枚举器接口 (enumerator interface) 为非泛型接口 System.Collections.IEnumerator 和泛型接口 System.Collections.Generic.IEnumerator<T> 的所有实例化。

简洁起见,将这些接口分别表示为 IEnumeratorIEnumerator<T>

可枚举 enumerable 接口 - IEnumerable


可枚举接口 (enumerable interface) 为非泛型接口 System.Collections.IEnumerable 和泛型接口 System.Collections.Generic.IEnumerable<T> 的所有实例化。

简洁起见,将这些接口分别表示为 IEnumerableIEnumerable<T>

产生类型 yield type


迭代器产生一系列值,所有值的类型均相同。此类型称为迭代器的产生类型 (yield type)。

  • 返回 IEnumeratorIEnumerable 的迭代器的产生类型是 object
  • 返回 IEnumerator<T> IEnumerable<T> 的迭代器的产生类型是 T

C# 1.0 使用 foreach 语句可以迭代集合,但创建枚举器需要大量代码。C# 2.0 添加了 yield 语句,便于创建枚举器。

枚举器 enumerator 对象 


如果返回枚举器接口类型的函数成员是使用迭代器块实现的,调用该函数成员不会立即执行迭代器块中的代码。而是先创建并返回一个枚举器对象 (enumerator object)。此对象封装了在迭代器块中指定的代码,并且在调用该枚举器对象的 MoveNext 方法时执行该迭代器块中的代码。枚举器对象具有下列特点:

  • 它实现了 IEnumeratorIEnumerator<T>,其中 T 为迭代器的产生类型。
  • 它实现了 System.IDisposable
  • 它以传递给该函数成员的实参值(如果存在)和实例值的副本进行初始化。
  • 它有四种可能的状态:运行前 (before)、运行中 (running)、挂起 (suspended) 和运行后 (after),并且初始状态为运行前 (before) 状态。

枚举器对象通常是编译器生成的枚举器类的一个实例,它封装了迭代器块中的代码,并实现了枚举器接口,但也可能实现其他方法。如果枚举器类由编译器生成,则该类将直接或间接嵌套在包含该函数成员的类中,它将具有私有可访问性,并且它将具有一个供编译器使用的保留名称。

枚举器对象可实现除上面指定的那些接口以外的其他接口。

下面的各节将描述由枚举器对象所提供的 IEnumerableIEnumerable<T> 接口实现的 MoveNextCurrentDispose 成员的确切行为。

请注意,枚举器对象不支持 IEnumerator.Reset 方法。调用此方法将导致引发 System.NotSupportedException

MoveNext 方法

枚举器对象的 MoveNext 方法封装了迭代器块的代码。调用 MoveNext 方法将执行迭代器块中的代码,并相应设置枚举器对象的 Current 属性。MoveNext 执行的具体操作取决于调用 MoveNext 时的枚举器对象的状态:

  • 如果枚举器对象的状态为运行前 (before),则调用 MoveNext 会:
    • 将状态更改为运行中 (running)。
    • 将迭代器块的形参(包括 this)初始化为实参值以及初始化该枚举器对象时所保存的实例值。
    • 从头开始执行迭代器块,直到执行被中断(如后文所述)。
  • 如果枚举器对象的状态为运行中 (running),则调用 MoveNext 的结果不确定。
  • 如果枚举器对象的状态为挂起 (suspended),则调用 MoveNext 将:
    • 将状态更改为运行中 (running)。
    • 将所有局部变量和形参(包括 this)的值恢复为迭代器块的执行上次挂起时保存的值。注意,这些变量所引用对象的内容可能自上次调用 MoveNext 之后已经发生更改。
    • 恢复执行紧跟在引起执行挂起的 yield return 语句后面的迭代器块,并一直继续,直到执行中断(如后文所述)。
  • 如果枚举器对象的状态为运行后 (after),则调用 MoveNext 将返回 false。

当 MoveNext 执行迭代器块时,可以采用四种方式来中断执行:通过 yield return 语句、通过 yield break 语句、到达迭代器块的末尾以及引发异常并将异常传播到迭代器块之外。

  • 当遇到 yield return 语句时:
    • 计算该语句中给出的表达式,隐式转换为产生类型,并赋给枚举器对象的 Current 属性。
    • 迭代器体的执行被挂起。所有局部变量和形参(包括 this)的值被保存,此 yield return 语句的位置也被保存。如果 yield return 语句在一个或多个 try 块内,则此时与之关联的 finally 块将会执行。
    • 枚举器对象的状态更改为挂起 (suspended)。
    • MoveNext 方法向其调用方返回 true,指示迭代成功前进至下一个值。
  • 当遇到 yield break 语句时:
    • 如果 yield break 语句在一个或多个 try 块内,则与之关联的 finally 块将执行。
    • 枚举器对象的状态更改为运行后 (after)。
    • MoveNext 方法向其调用方返回 false,指示迭代完成。
  • 当遇到迭代器体的结束处时:
    • 枚举器对象的状态更改为运行后 (after)。
    • MoveNext 方法向其调用方返回 false,指示迭代完成。
  • 当引发异常并传播到迭代器块之外时:
    • 通过异常传播机制执行迭代器体内的相应 finally 块。
    • 枚举器对象的状态更改为运行后 (after)。
    • 异常继续传播至 MoveNext 方法的调用方。

Current 属性

枚举器对象的 Current 属性将受迭代器块中的 yield return 语句影响。

当枚举器对象处于挂起 (suspended) 状态时,Current 的值为上一次调用 MoveNext 时设置的值。当枚举器对象处于运行前 (before)、运行中 (running) 或运行后 (after) 状态时,访问 Current 的结果不确定。

对于产生类型不是 object 的迭代器,通过枚举器对象的 IEnumerable 实现来访问 Current 的结果对应于通过枚举器对象的 IEnumerator<T> 实现来访问 Current 并将该结果强制转换为 object。

Dispose 方法

Dispose 方法用于通过使枚举器对象变为运行后 (after) 状态来清除迭代。

  • 如果枚举器对象的状态为运行前 (before),则调用 Dispose 将把状态更改为运行后 (after)。
  • 如果枚举器对象的状态为运行中 (running),则调用 Dispose 的结果不确定。
  • 如果枚举器对象的状态为挂起 (suspended),则调用 Dispose 将:
    • 将状态更改为运行中 (running)。
    • 执行所有 finally 块,就像最后执行的 yield return 语句是 yield break 语句一样。如果这导致引发异常,并且异常传播到迭代器体之外,则枚举器对象的状态设置为运行后 (after),并且将异常传播到 Dispose 方法的调用方。
    • 将状态更改为运行后 (after)。
  • 如果枚举器对象的状态为运行后 (after),则调用 Dispose 没有任何作用。

可枚举对象 enumerable


如果返回可枚举接口类型的函数成员是使用迭代器块实现的,调用该函数成员不会立即执行迭代器块中的代码。而是先创建并返回一个可枚举对象 (enumerable object)。可枚举对象的 GetEnumerator 方法返回一个封装有迭代器块中指定的代码的枚举器对象,当调用该枚举器对象的 MoveNext 方法时,将执行迭代器块中的代码。可枚举对象具有下列特点:

  • 它实现了 IEnumerableIEnumerable<T>,其中 T 为迭代器的产生类型。
  • 它以传递给该函数成员的实参值(如果存在)和实例值的副本进行初始化。

可枚举对象通常是编译器生成的可枚举类的实例,它封装了迭代器块中的代码,并实现了可枚举接口,但也可能实现其他方法。如果可枚举类由编译器生成,则该类将直接或间接嵌套在包含该函数成员的类中,它将具有私有可访问性,并且它将具有供编译器使用的保留名称。

可枚举对象可实现除上面指定的那些接口以外的其他接口。具体而言,可枚举对象还可实现 IEnumerator 和 IEnumerator<T>,从而使其既可作为可枚举对象,也可作为枚举器对象。在该类型的实现中,首次调用可枚举对象的 GetEnumerator 方法时,将返回可枚举对象本身。对可枚举对象的 GetEnumerator 的后续调用(如果存在),将返回可枚举对象的副本。因此,每个返回的枚举器都有自己的状态,一个枚举器中的更改不会影响其他枚举器。

GetEnumerator 方法

可枚举对象实现了 IEnumerable 和 IEnumerable<T> 接口的 GetEnumerator 方法。这两种 GetEnumerator 方法的实现是相同的,都是获取并返回一个可用的枚举器对象。枚举器对象是以初始化该可枚举对象时保存的实例值和实参值进行初始化的,此外,枚举器对象函数如前所述。

示例


下面的 Stack<T> 类使用一个迭代器实现其 GetEnumerator 方法。

规范中的该示例有编译错误,所以采用 MSDN 中的实例:

public class Stack<T> : IEnumerable<T>
{
    PRivate T[] items;
    private int count;
 
    public void Push(T item)
    {
        if (items == null)
        {
            items = new T[4];
        }
        else if (items.Length == count)
        {
            T[] newItems = new T[count * 2];
            Array.Copy(items, 0, newItems, 0, count);
            items = newItems;
        }
        items[count++] = item;
    }
 
    public T Pop()
    {
        T result = items[--count];
        items[count] = default(T);
        return result;
    }
 
    public IEnumerator<T> GetEnumerator()
    {
        for (int i = count - 1; i >= 0; --i)
            yield return items[i];
    }
 
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
 
    public IEnumerable<T> TopToBottom
    {
        get { return this; }
    }
 
    public IEnumerable<T> BottomToTop
    {
        get
        {
            for (int index = 0; index <= count - 1; index++)
            {
                yield return items[index];
            }
        }
    }
 
    public IEnumerable<T> TopN(int itemsFromTop)
    {
        // Return less than itemsFromTop if necessary. 
        int startIndex = itemsFromTop >= count ? 0 : count - itemsFromTop;
 
        for (int index = count - 1; index >= startIndex; index--)
        {
            yield return items[index];
        }
    }
}

若用如下代码:

Stack<int> stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
stack.Push(3);
stack.Push(4);
stack.Push(5);
foreach (int s in stack)
{
    Console.Write(s + " ");
}
stack.Pop();
Console.Write("\n");
foreach (int s in stack)
{
    Console.Write(s + " ");
}
Console.Write("\n");
foreach (int s in stack.TopToBottom)
{
    Console.Write(s + " ");
}
Console.Write("\n");
foreach (int s in stack.BottomToTop)
{
    Console.Write(s + " ");
}
Console.Write("\n");
foreach (int s in stack.TopN(2))
{
    Console.Write(s + " ");
}
 
Console.ReadKey();

运行结果:

5 4 3 2 1
4 3 2 1
4 3 2 1
1 2 3 4
4 3

参考资料


  • MSDN Iterators (C# and Visual Basic)
  • yield (C# Reference)
  • foreach, in (C# Reference)
  • How to: Access a Collection Class with foreach (C# Programming Guide)
  • C#5.0 规范-中文
  • C#5.0 规范-英文

 

下载 Demo