出处:http://www.ondotnet.com/pub/a/dotnet/2004/06/07/liberty.html
术语表
Iterator:枚举器
如果你正在创建一个表现和行为都类似于集合的类,允许类的用户使用 foreach 语句对集合
中的成员进行枚举将会是很方便的。这在 C# 2.0 中比 C# 1.1 更容易实现一些。作为演示,我
们先在 C# 1.1 中为一个简单的集合添加枚举,然后我们修改这个范例,使用新的 C#2.0 枚举构
建方法。
我们将以创建一个简单化的 List Box 作为开始,它将包含一个 8 字符串的数组和一个整型,
这个整型用于记录数组中已经添加了多少字符串。构造函数将对数组进行初始化并使用传递进来
的参数填充它。
public ListBox(params string[] initialStrings)
{
strings = new String[8];
foreach (string s in initialStrings)
{
strings[ctr++] = s;
}
}
除此以外,ListBox 类还需要一个 Add方法(进行添加 string 的操作) 和 一个返回数组中
字符串个数的方法。
public void Add(string theString)
{
strings[ctr] = theString;
ctr++;
}
public int GetNumEntries()
{
return ctr;
}
NOTE:实际开发中,通常使用 ArrayList,而不是固定大小的数组。在这里为了程序简单就
没有做数组下标越界的检测。
从感觉上看,ListBox 像是一个集合,如果可以使用集合中通常使用的 foreach 循环来获
取 listBox中的所有字符串将会是非常便利的。如此的话,可以这样书写代码:
ListBox lb = new ListBox("a", "b", "c", "d", "e", "f", "g", "h");
foreach (string s in lb) {
Console.WriteLine(s);
}
但是,会得到这样一个错误:
“Iterator.ListBox”不 包 含 “GetEnumerator”的 公 共 定 义 , 因 此 foreach 语 句 不 能 作 用 于
“Iterator.ListBox”类型的变量
想要使用 foreach 语句,还必须实现 IEnumerable 接口。
这个接口只要求实现一个方法: GetEnumerator。这个方法必须返回一个实现了
IEnumerator 接口的对象。除此以外,我们需要返回的这个对象不仅实现了 IEnumerator,而且
知道如何枚举 ListBox对象。你将需要创建一个 ListBoxEmunerator(在下面描述):
NOTE: IEnumerable 和 IEnumerator 是不同的接口,请不要搞混了。
public IEnumerator GetEnumerator()
{
return new ListBoxEnumerator();
}
现在,ListBox 可以使用 foreach 循环了:
ListBox lbt = new ListBox("Hello", "World");
lbt.Add("Who");
lbt.Add("Is");
lbt.Add("John");
lbt.Add("Galt");
foreach (string s in lbt)
{
Console.WriteLine("Value: {0}", s);
}
先是实例化这个 ListBox ,并初始了两个字符串,随后又添加了四个。foreach 循环接受
ListBox 实例,并且迭代它,依次返回字符串。输出是:
Hello
World
Who
Is
John
Galt
实现 IEnumerator 接口
注意到 ListBoxEnumerator 不仅需要实现 IEnumerator 接口,对于 ListBox类它也需要一些
特别了解;特别是,它必须可以获得 ListBox 的字符串数组并且遍历其所包含的字符串。
IEnumerable 类和与其相关的 IEnumerator 类之间的关系有一点微妙。实现 IEnumerator 接口
的最好办法是在 IEnumerable 类里创建一个嵌套的 IEnumerator 类。
public class ListBox : IEnumerable
{
// 嵌套的私有 ListBoxEnumerator类实现
private class ListBoxEnumerator : IEnumerator
{
// 代码实现
}
// ListBox类的代码
}
注意 ListBoxEnumerator 需要对它所嵌入的 ListBox 类的一个引用。你可以通过
ListBoxEnumerator 的构造函数来传递。
为了实现 IEnumerator 接口,ListBoxEnumerator需要两个方法:MoveNext 和Reset,还有
一个属性:Current。这些方法和属性的任务是创建一个状态机制,确保你可以在任何时候得知
ListBox 中的哪个元素是当前元素,并获得那个元素。
在这个例子中,这种状态机制是通过维护一个标明当前 string 的索引值来完成的,并且,
你可以通过对外部类的 string 集合进行索引来返回这个当前的 string。为了达到这个目标,你
需要一个成员变量保存对于外部 ListBox 对象的引用,以及一个整型用于保存当前索引。
private ListBox lbt;
private int index;
每次 Reset方法被调用的时候,index 被置为 -1。
public void Reset()
{
index = -1;
}
每次 MoveNext 被调用的时候,外部类的数组检查时候已经到了末尾,如果是这样,方法返
回 false。如果集合中还有对象,index 将增加,并且方法返回 true。
public bool MoveNext()
{
index++;
if (index >= lbt.strings.Length)
{
return false;
}else
{
return true;
}
}
最后,如果 MoveNext 方法返回 True,foreach 循环将调用 Current 属性。ListBoxEnumerator
的 Current属性的实现是索引外部类(ListBox)中的集合,并且返回找到的对象(这个例子中,是
一个字符串)。注意,返回一个 Object 是因为IEnumerator 接口中 Current 属性的签名如此。
public object Current
{
get {
return(lbt[index]);
}
}
在 1.1 中,所有想要通过 foreach循环来迭代的类都需要实现 IEnumerable 接口,于是,必
须创建一个实现了 IEnumerator 的类。最糟的是,enumerator 返回的值并不是类型安全的。记
得 Current 属性返回一个 Object 对象;它仅仅简单的假设你所返回的值与 foreach 循环所期望
的相符合。
C# 2.0 的解救办法
使用 C# 2.0 这些问题如同五月末的雪般融化了。在这个例子的 2.0 版本中,我重写上面的
列表,使用 C# 2.0 的两个新特性:泛型 和 枚举器。
我以重新定义实现 IEumerable<string>的ListBox 作为开始:
public class ListBox : IEnumerable<string>
这样做确定这个类可以在 foreach循环中使用,同时确保迭代的值是 string 类型。
现在,从上个例子中挪去整个嵌套类,并且用下面的代码替换 GetEnumerator 方法。
public IEnumerator<string> GetEnumerator()
{
foreach (string s in strings)
{
yield return s;
}
}
GetEnumerator 方法使用了新的 yield 语句。yield 语句返回一个表达式。yield 语句仅在
迭代块中出现,并且返回 foreach语句所期望的值。那也就是,对 GetEnumerator的每次调用都
将会产生集合中的下一个字符串;所有的状态管理已经都为你做好了!
就这样了,你已经完成了。不需要为每个类型实现你自己的 enumerator,不需要创建嵌套
类。你已经移除了至少 30 行代码,并且极大地简化了你的代码。程序继续像期望的那样运行,
但是状态管理不再是你的任务,所有的都为你做好了。更进一步,由枚举器所返回的值一定是
string 类型,如果你想要返回其他类型,你可以修改 IEnumerable 泛型语句,IEnumerable 泛型
语句将反射新类型。
关于Yield 的更多内容
作为对上一节的一些说明,应该告诉你:实际上,你可以在 yield 语句块中 yield 一个以上
的值。这样,下面的语句是完全正确的 C#语句:
public IEnumerator GetEnumerator()
{
yield return "Who";
yield return " is";
yield return "John Galt?";
}
假设上面的代码位于一个名为 foo的类中,你可以这样写:
foreach ( string s in new foo())
{
Console.Write(s);
}
输出结果将会是:
Who is John Galt?
如果你现在停下来思考一下,这些也是之前的代码所做的事。它遍历了自己的 foreach 循环,
并且产生出它所找到的每个 string字符串。
本文的源代码可以在 http://www.tracefact.net/SourceCode/Iterators-In-CSharp.rar
下载。
随笔 - 164
文章 - 0
评论 - 19
阅读 -
11万
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)