65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 迭代器模式揭秘

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (17投票s)

2012 年 4 月 4 日

CPOL

8分钟阅读

viewsIcon

82075

这是“如何使用 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# 迭代器的大部分知识都来自那本书。顺便说一句:他还很好地描述了其他主题,如协变和逆变等。简直太棒了!

在线链接

更多关于 LINQ 的内容?

LINQ 是一个非常广泛的话题,我没有在本技巧中涵盖。这里感兴趣的部分是提供的扩展方法。一些链接可以发现技术细节。

享受探索迭代器世界并欣赏其力量吧!

干杯

安迪

历史

V1.02012-04-04初始版本。
V1.12012-04-04对引言中的文本做了微小修改——采纳了 Jani 的建议。
V1.22012-04-05修正了一个错别字,提到了 IQueryable,添加了“接下来去哪里......?”部分。
V1.32012-04-05添加了 Matt 建议的链接。
V1.42012-04-10重新添加了一些“神奇地”删除的高亮显示。
© . All rights reserved.