C# 迭代器模式揭秘






4.96/5 (17投票s)
这是“如何使用 IEnumerable/IEnumerator 接口”的替代方案
引言
这是如何使用 IEnumerable/IEnumerator 接口[^] 的替代方案。这个替代技巧的目的是为初学者提供更多相关信息,以及为什么人们应该关注迭代器。
涵盖的主题是
- C# 中的迭代器模式
- 什么是迭代器?
- 我的第一个迭代器
- IEnumerable/IEnumerable<T> 是做什么用的?
- 我还能用迭代器做什么?
- 就这些,伙计们!(又名总结)
- 接下来去哪里......?
让我们从这里开始:为什么要关注迭代器模式?你很可能在日常工作中使用了迭代器模式,但可能没有意识到。
List<string> all = new List<string>() { "you", "me", "everyone" };
...
foreach(string who in all)
{
Console.WriteLine("who: {0}!", who);
}
迭代器告诉 foreach
循环你获取元素的顺序。
C# 中的迭代器模式
这就是 C# 中的迭代器模式一个可以在
foreach
循环中使用的类必须提供一个 IEnumerator<T> GetEnumerator() { ... }
方法。该方法名是为此目的保留的。此函数定义了元素返回的顺序。稍后会详细介绍。注意:一些类也可能提供非泛型 IEnumerator GetEnumerator() { ... }
方法。这来自于没有泛型的旧时代,例如,所有非泛型集合如 Array 等都只提供这种“老式”迭代器函数。
在幕后,foreach 循环
foreach(string who in all) { ... }
转换为
显式泛型版本 | 显式非泛型版本 |
---|---|
using (var it = all.GetEnumerator())
while (it.MoveNext())
{
string who = it.Current;
...
} |
var it = all.GetEnumerator();
while (it.MoveNext())
{
string who = (string)it.Current;
...
}
|
额外内容:这两个显式迭代器调用可以合并为一个
var it = all.GetEnumerator();
using(it as IDisposable)
while (it.MoveNext())
{
string who = (string)it.Current;
...
}
啊,是的,你现在会喜欢 foreach
循环了!它消除了编写这一大堆显式迭代器代码的负担。请注意:你可以以上述任何一种形式编写,但毫无疑问,只要可能,你都会使用 foreach
循环,对吧?
因此,C# 迭代器模式实现的核心是 GetEnumerator()
方法。那么这些 IEnumerator
/IEnumerator<T>
接口是什么呢?
什么是迭代器
迭代器提供了一种遍历(即循环)某些项的方法。元素的序列由 IEnumerator
/IEnumerator<T>
接口的实现给出。
namespace System.Collections
{
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
}
namespace System.Collections.Generic
{
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
T Current { get; }
}
}
该模式基本上由 MoveNext()
和 Current
给出。其语义是必须首先调用 MoveNext()
才能到达第一个元素。如果 MoveNext()
返回 false
,则不再有元素。Current
返回当前元素。如果之前的 MoveNext()
返回 false,则不应调用 Current
。
MoveNext()
按元素序列给出下一个元素——无论该序列是什么,例如从第一个到最后一个,或按某些标准排序,或随机等等。
你现在知道如何应用迭代器模式(例如在 foreach
循环中),并且知道所有提供上述 GetEnumerator()
方法(迭代器)的类都可以做到这一点。
如何编写自己的迭代器?继续阅读。
我的第一个迭代器
如何编写自己的迭代器?你可能会说,没有什么比这更容易的了:实现 IEnumerator<T>
接口并在 GetEnumerator()
方法中返回该类的实例。
这将是“硬方法”。简单的方法是使用 yield return
——C# 编写迭代器的方式。
public class MyData
{
private string _id;
private List<string> _data;
public MyData(string id, params string[] data)
{
_id = id;
_data = new List<string>(data);
}
public IEnumerator<string> GetEnumerator()
{
yield return _id;
foreach(string d in _data) yield return d;
}
}
yield return
在 GetEnumerator() 方法中的关键概念是定义 MoveNext()
遍历的序列。每个 yield return
调用代表序列中的一步,并带有相应的 Current
值。
迭代器的一个等效显式实现可能看起来像这样
public class MyData
{
private string _id;
private List<string> _data;
public MyData(string id, params string[] data)
{
_id = id;
_data = new List<string>(data);
}
public class MyEnumerator: IEnumerator<string>
{
private MyData _inst;
private int _pos;
internal MyEnumerator(MyData inst) { Reset(); _inst = inst; }
public string Current
{
get
{
if (_pos == -1) return _inst._id;
if (0 <= _pos && _pos < _inst._data.Count()) return _inst._data[_pos];
return default(string);
}
}
public void Dispose() { }
object IEnumerator.Current { get { return Current; } }
public bool MoveNext() { return ++_pos < _inst._data.Count(); }
public void Reset() { _pos = -2; }
}
public IEnumerator<string> GetEnumerator() { return new MyEnumerator(this); }
}
你为什么要显式地这样做?没有很好的理由,除非你的 C# 版本不提供 yield
关键字(这只有在你处于 C# 石器时代时才成立 ;-))。如果序列比此处显示的更复杂,yield
的优势会变得更加明显。我把这个练习留给你来玩迭代器序列......
你现在知道如何通过利用 yield
的强大功能来编写自己的迭代器了。
然而,你会发现你几乎从不编写自己的迭代器。原因在于 IEnumerable
/IEnumerable<T>
接口和 LINQ 的出现。
IEnumerable/IEnumerable<T> 是做什么用的?
这些接口非常简单
namespace System.Collections
{
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
}
namespace System.Collections.Generic
{
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
}
所以,简单的答案是:它们提供了一个迭代器(一个“老式”的,一个带泛型的)。
实现了其中一个接口的类提供了一个迭代器实现。此外,这样的实例可以在需要其中一个接口的任何地方使用。注意:拥有迭代器不需要实现此接口:可以在不实现此接口的情况下提供其 GetEnumerator()
方法。但在这种情况下,不能将该类传递给需要传递 IEnumerable<T>
的方法。
例如,List<T>
有一个构造函数,它接受一个 IEnumerable<T>
来从该迭代器初始化其内容。
namespace System.Collections.Generic
{
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable
{
...
public List(IEnumerable<T> collection);
...
}
}
如果你现在查看 LINQ 扩展方法:其中许多都基于 IEnumerable<T>
,因此,通过一些新函数扩展任何迭代器类,这些函数通常会返回另一个迭代器。例如
namespace System.Linq
{
public static class Enumerable
{
...
public static IEnumerable<TResult> Select<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector);
...
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate);
...
}
}
用法如下
List<string> list = ...;
var query = list.Where(s=>s.Length > 2).Select(s=>s);
foreach(string s in query)
{
...
}
C# 语言再次提供了另一种表达方式(可以说更简单)
List<string> list = ...;
var query = from s in list where s.Length > 2 select s;
foreach(string s in query)
{
...
}
这就是 LINQ - Language Integrated Queries(语言集成查询):扩展方法可以表示为 from ... in ... where ... select
的形式(仅展示一些 LINQ 关键字)。请注意,您始终可以将 LINQ 表达式编写为上面所示的扩展方法链。
现在你已经了解了 IEnumerable<T>
接口的优点以及它们的用途和方式。
我还能用迭代器做什么?
您可以在任何类上定义自己的特定迭代器。以下代码
foreach(var item in data) { ... }
使用数据类的 GetEnumerator()
。
你对此有什么看法?
foreach(var item in data.Random(1000)) { ... }
是的,表达式 data.Random(1000)
必须提供一个迭代器,即一个 GetEnumerator()
方法。这意味着数据类的 Random(int n)
方法必须返回 IEnumerable<T>
(或其非泛型同级)。
public class MyData
{
...
public IEnumerable<string> Random(int n)
{
...
}
...
}
该方法的正文是什么?您可以在这里再次使用 yield
关键字来实现迭代器的 MoveNext()
/Current
对。
public IEnumerable<string> Random(int n)
{
Random random = new Random(_data.Count);
while(n-- > 0) yield return _data[random.Next()];
}
就这些,伙计们!
- C# 中的迭代器模式由返回
IEnumerator
或其泛型同级(你选择)的GetEnumerator()
方法给出。 IEnumerable
(或其泛型同级)实现提供了一个迭代器(因此,GetEnumerator()
方法)。- 返回
IEnumerator
/IEnumerable
或其泛型同级的方法和属性可以通过yield return
调用方便地实现。 - 迭代器是 LINQ 的基本要素之一(其他要素是 lambda 表达式形式的委托和 IQueriable<T>/IQueriable - 但这是另一个关于它们用途的故事)。
- 您很可能不会实现自己的迭代器,因为 LINQ 提供了强大的基于迭代器的扩展方法(选择、排序、聚合、计数、计算等),但是如果您对 C# 中实现的迭代器概念有基本的了解,也不会有坏处。
接下来去哪里......?
想了解更多关于 C# 迭代器的信息吗?
我是一个书籍爱好者(如果一本书有几页关键内容值得一读,我就会买下它 ;-))。
- 因此,首先我推荐一本精彩的书籍:Kompaktkurs C# 4.0[^]。它只有德语版本。作者是Hanspeter Mössenböck[^]。他以非常简洁的风格描述了每个 C# 语言特性的核心。我所描述的迭代器就是受他的著作启发。我关于 C# 迭代器的大部分知识都来自那本书。顺便说一句:他还很好地描述了其他主题,如协变和逆变等。简直太棒了!
在线链接
- 当然,微软也有关于迭代器和
yield
的文章,但它们技术性强,缺乏大局观。
迭代器 (C# 编程指南)[^]
使用迭代器 (C# 编程指南)[^]
yield (C# 参考)[^] - 来自《深入理解 C# 第二版》[^] 的迭代器块实现细节:自动生成的状态机[^] 提供了关于
yield
生成内容的更多细节(感谢Matt[^] 提供这个提示[^]!) - 关于迭代器和
yield
的在线文档有很多,但我没有找到一篇像上面提到的书那样精确地描述幕后机制的。 - 如果你能访问 .Net Reflector[^] 或 IL-Spy[^],你可能会逆向工程出
foreach
循环创建的结构。 - 你可能会在
foreach
循环之外看到迭代器在起作用:在发明你自己的动态 LINQ 解析器[^]中,你会看到迭代器被用作标记序列。这些标记(本例中是字符串)由正则表达式生成并以迭代器形式提供,这样就可以一个接一个地获取标记,直到不再有标记为止。
更多关于 LINQ 的内容?
LINQ 是一个非常广泛的话题,我没有在本技巧中涵盖。这里感兴趣的部分是提供的扩展方法。一些链接可以发现技术细节。
- 来自 MSDN 杂志[^] 的 LINQ Enumerable 类,第一部分[^](感谢 Matt[^] 提供此提示[^]!)
- IEnumerator(Of T) 接口[^](迭代器)
- IEnumerable(Of T) 接口[^](也列出了所有扩展方法,主要来自
Enumerable
类) - Enumerable 类[^](包含
IEnumerable<T>
扩展方法) - Queryable 类[^](包含
IQueryable<T>
扩展方法) - delegate (C# 参考)[^](又是微软式的:缺少大局观——非常技术化)
- Lambda 表达式 (C# 编程指南)[^]
享受探索迭代器世界并欣赏其力量吧!
干杯
安迪
历史
V1.0 | 2012-04-04 | 初始版本。 |
V1.1 | 2012-04-04 | 对引言中的文本做了微小修改——采纳了 Jani 的建议。 |
V1.2 | 2012-04-05 | 修正了一个错别字,提到了 IQueryable,添加了“接下来去哪里......?”部分。 |
V1.3 | 2012-04-05 | 添加了 Matt 建议的链接。 |
V1.4 | 2012-04-10 | 重新添加了一些“神奇地”删除的高亮显示。 |