有人能为我解释一下IEnumerable和IEnumerator吗?

341

有人能向我解释一下 IEnumerableIEnumerator 吗?

例如,什么时候应该使用它而不是 foreach?IEnumerableIEnumerator 之间的区别是什么?为什么我们需要使用它?


79
自Java和JavaScript以来,这是最糟糕的命名混淆。 - Matthew Lock
7
@MatthewLock,你能解释一下你的意思吗? - David Klempfner
16个回答

329
举个例子,何时使用它而不是foreach?
你不会使用IEnumerable "over" foreach。实现IEnumerable使得使用foreach成为可能。
当你编写这样的代码时:
foreach (Foo bar in baz)
{
   ...
}

这在功能上等同于写作:

IEnumerator bat = baz.GetEnumerator();
while (bat.MoveNext())
{
   Foo bar = (Foo)bat.Current;
   ...
}

“功能上等同”指的是编译器将代码转换成的实际形式。在这个例子中,除非“baz”实现了“IEnumerable”,否则无法在“baz”上使用“foreach”。
“IEnumerable”意味着“baz”实现了该方法。
IEnumerator GetEnumerator()

这个方法返回的IEnumerator对象必须实现这些方法。
bool MoveNext()

并且

Object Current()

第一种方法是在创建枚举器的IEnumerable对象中前进到下一个对象,如果完成则返回false,第二种方法返回当前对象。
在.NET中,任何可以迭代的东西都实现了IEnumerable接口。如果你正在构建自己的类,并且它没有继承实现IEnumerable接口的类,你可以通过实现IEnumerable接口(并创建一个枚举器类,其新的GetEnumerator方法将返回该类)使你的类可以在foreach语句中使用。

15
我认为原作者所说的“over foreach”,意思是“何时应该显式调用GetEnumerator() / MoveNext()而不是使用foreach循环。”有价值的提示。 - mqp
6
啊,我想你是对的。嗯,我的帖子中已经隐含了答案:这没有关系,因为编译器会自动处理。所以做最容易的事情,也就是使用foreach。 - Robert Rossney
12
这个答案没有讲述完整的故事。可枚举对象不需要实现 IEnumerable 接口;它们只需要有一个 GetEnumerator 方法,该方法返回一个类型的实例,该类型再拥有 bool MoveNext() 方法和任意类型的 Current 属性。这种“鸭子类型”方法在 C# 1.0 中被实现,用于遍历值类型集合时避免装箱。 - phoog
3
有了上面的解释并且仔细阅读实现 (T) 的 IEnumerable 的步骤,你有了很好的资源。 - ruedi
4
这有点像 C++ 中的“迭代器(iterator)”吗? - Matt G
显示剩余5条评论

198

IEnumerable 和 IEnumerator 接口

为了开始研究如何实现已有的 .NET 接口,让我们先了解一下 IEnumerable 和 IEnumerator 的作用。回想一下,C# 支持一个名为 foreach 的关键字,它允许你遍历任何数组类型的内容:

// Iterate over an array of items.
int[] myArrayOfInts = {10, 20, 30, 40};
foreach(int i in myArrayOfInts)
{
   Console.WriteLine(i);
}

虽然看起来好像只有数组类型才能使用这个结构,但事实上任何支持名为GetEnumerator()的方法的类型都可以被foreach结构所评估。为了说明这一点,跟着我来!

假设我们有一个Garage类:

// Garage contains a set of Car objects.
public class Garage
{
   private Car[] carArray = new Car[4];
   // Fill with some Car objects upon startup.
   public Garage()
   {
      carArray[0] = new Car("Rusty", 30);
      carArray[1] = new Car("Clunker", 55);
      carArray[2] = new Car("Zippy", 30);
      carArray[3] = new Car("Fred", 30);
   }
}

理想情况下,使用foreach结构可以方便地迭代Garage对象的子项,就像一个数据值数组一样:

// This seems reasonable ...
public class Program
{
   static void Main(string[] args)
   {
      Console.WriteLine("***** Fun with IEnumerable / IEnumerator *****\n");
      Garage carLot = new Garage();
      // Hand over each car in the collection?
      foreach (Car c in carLot)
      {
         Console.WriteLine("{0} is going {1} MPH",
         c.PetName, c.CurrentSpeed);
      }
      Console.ReadLine();
   }
}

可悲的是,编译器告诉你Garage类没有实现名为GetEnumerator()的方法。这个方法由IEnumerable接口正式化,该接口隐藏在System.Collections命名空间中。 支持此行为的类或结构声明它们能够向调用者(在本例中为foreach关键字本身)公开包含的子项。这是此标准.NET接口的定义:

// This interface informs the caller
// that the object's subitems can be enumerated.
public interface IEnumerable
{
   IEnumerator GetEnumerator();
}

正如你所看到的,GetEnumerator()方法返回对另一个名为System.Collections.IEnumerator的接口的引用。该接口提供基础设施,以允许调用者遍历IEnumerable兼容容器中包含的内部对象:

// This interface allows the caller to
// obtain a container's subitems.
public interface IEnumerator
{
   bool MoveNext (); // Advance the internal position of the cursor.
   object Current { get;} // Get the current item (read-only property).
   void Reset (); // Reset the cursor before the first member.
}

如果你想更新车库类型以支持这些接口,你可以走一条漫长的路,并手动实现每个方法。虽然你当然可以提供自定义版本的GetEnumerator()、MoveNext()、Current和Reset(),但有一种更简单的方法。由于System.Array类型(以及许多其他集合类)已经实现了IEnumerable和IEnumerator,因此你可以简单地将请求委托给System.Array,如下所示:

using System.Collections;
...
public class Garage : IEnumerable
{
   // System.Array already implements IEnumerator!
   private Car[] carArray = new Car[4];
   public Garage()
   {
      carArray[0] = new Car("FeeFee", 200);
      carArray[1] = new Car("Clunker", 90);
      carArray[2] = new Car("Zippy", 30);
      carArray[3] = new Car("Fred", 30);
   }
   public IEnumerator GetEnumerator()
   {
      // Return the array object's IEnumerator.
      return carArray.GetEnumerator();
   }
}

在更新了你的车库类型之后,你可以在C#的foreach语句中安全地使用该类型。此外,由于GetEnumerator()方法已经被公开定义,用户对象也可以与IEnumerator类型进行交互:

// Manually work with IEnumerator.
IEnumerator i = carLot.GetEnumerator();
i.MoveNext();
Car myCar = (Car)i.Current;
Console.WriteLine("{0} is going {1} MPH", myCar.PetName, myCar.CurrentSpeed);

然而,如果您希望从对象级别隐藏IEnumerable的功能,则可以简单地利用显式接口实现:

IEnumerator IEnumerable.GetEnumerator()
{
  // Return the array object's IEnumerator.
  return carArray.GetEnumerator();
}

这样做,普通的对象用户将找不到车库的GetEnumerator()方法,而foreach结构会在必要时在后台获取接口。

摘自《Pro C# 5.0和.NET 4.5 Framework》


2
非常棒的答案,谢谢!不过我有一个问题。在第一个例子中,您使用foreach循环遍历数组,但在第二个例子中却不能这样做。这是因为数组在类中还是因为它包含对象? - Caleb Palmquist
2
谢谢您的详细解释。我唯一的问题是为什么不在Garage中创建一个获取carArray的方法呢?这样您就不必自己实现GetEnumerator,因为Array已经实现了它。例如:foreach(Car c in carLot.getCars()) { ... } - Nick Rolando
这样的回答值得两个赞,而不是一个... - Marcelo Scofano Diniz
虽然代码显示了重点,但值得一提的是:1-上面代码中的Garage只是类的构造函数。构造函数不负责向调用方返回数据。2-我们需要调用一个返回汽车数组的方法。在现实世界中,应该在Garage中编写一个返回数组的方法,并返回一个数组,在这种情况下,就不需要使用IEnumerable了。 - NoChance
有史以来最佳答案! - M.KD

70

实现IEnumerable意味着你的类返回一个IEnumerator对象:

public class People : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator()
    {
        // return a PeopleEnumerator
    }
}

实现IEnumerator意味着您的类返回用于迭代的方法和属性:

public class PeopleEnumerator : IEnumerator
{
    public void Reset()...

    public bool MoveNext()...

    public object Current...
}

这就是区别。


1
很好,这解释了(连同其他帖子一起)如何将您的类转换为可以使用“foreach”迭代其内容的类。 - Contango

63

类比说明 + 代码演示

类比:想象你是一名在飞机上的侦探。你需要穿过所有乘客,找到你的嫌疑人。

要做到这一点,飞机必须满足以下条件:

  1. 可枚举的,
  2. 如果它有一个计数器。

什么是“可枚举的”?

如果一个航空公司是“可枚举的”,这意味着必须有一名空中乘务员在飞机上,其唯一工作是计数:

  1. 计数器/空中乘务员必须在第一位乘客之前开始(即空中乘务员)必须向前移动到第一个座位。
  2. 然后,他/她记录:(i)座位上的人是谁,(ii)他们在过道上的当前位置

计数器继续进行,直到达到飞机末端。

让我们将此与IEnumerables联系起来

    foreach (Passenger passenger in Plane)
    // the airline hostess is now at the front of the plane
    // and slowly making her way towards the back
    // when she get to a particular passenger she gets some information
    // about the passenger and then immediately heads to the cabin
    // to let the captain decide what to do with it
    { // <---------- Note the curly bracket that is here.
        // we are now cockpit of the plane with the captain.
        // the captain wants to give the passenger free 
        // champaign if they support manchester city
        if (passenger.supports_mancestercity())
        {
            passenger.getFreeChampaign();
        } 
        else
        {
            // you get nothing! GOOD DAY SIR!
        }
    } //  <---- Note the curly bracket that is here!
      //  the hostess has delivered the information 
      //  to the captain and goes to the next person
      //  on the plane (if she has not reached the 
      //  end of the plane)

摘要

换句话说,如果某物品有计数器,则它是可数的。 计数器必须具备以下基本功能:(i)记住其位置(状态),(ii)能够移动到下一个,(iii)并了解它正在处理的当前人员

Enumerable只是“可数”的花哨说法。


25

IEnumerable 实现了 GetEnumerator 方法。当调用此方法时,它将返回一个实现了 MoveNext、Reset 和 Current 方法的IEnumerator 对象。

因此,当您的类实现 IEnumerable 接口时,您就是在表示您可以调用一个方法(GetEnumerator),并返回一个新的对象(即 IEnumerator),您可以在诸如 foreach 循环之类的循环中使用该对象。


18

实现IEnumerable使您可以获取列表的IEnumerator。

IEnumerator允许使用yield关键字以foreach方式顺序访问列表中的项。

在foreach实现(例如在Java 1.4中)之前,迭代列表的方法是从列表获取枚举器,然后要求它返回列表中的“下一个”项,只要返回值作为下一个项不为null。 foreach只是将其隐式实现为语言特性,就像lock()在幕后实现Monitor类一样。

我期望foreach可以在列表上工作,因为它们实现了IEnumerable。


如果这是一个列表,为什么我不能只使用foreach呢? - prodev42
.Net中的foreach语句使用鸭子类型,但IEnumerable/IEnumerable<T>是表明你的类可以被枚举的适当方式。 - user7116

18
  • 实现IEnumerable接口的对象允许其他人通过枚举器访问其中的每个项目。
  • 实现IEnumerator接口的对象正在执行迭代。它正在循环遍历一个可枚举对象。

将可枚举对象视为列表、堆栈、树等。


15

IEnumerableIEnumerator(以及它们的泛型对应物IEnumerable<T>和IEnumerator<T>)是迭代器.Net Framework Class Libray collections中的基本接口。

IEnumerable是大多数代码中最常见的接口。它使得foreach循环、生成器(想一下yield)成为可能,由于其小巧的接口,它被用来创建紧凑的抽象层。IEnumerable依赖于IEnumerator

另一方面,IEnumerator提供了稍微低级的迭代接口。它被称为显式迭代器,这使得程序员可以更好地控制迭代过程。

IEnumerable

IEnumerable是一个标准接口,它使得可以遍历支持它的集合(事实上,我今天能想到的所有集合类型都实现了IEnumerable)。编译器支持语言特性,如foreach。一般来说,它使得隐式迭代器实现成为可能(具体请参见此处)

foreach循环

foreach (var value in list)
  Console.WriteLine(value);

我认为foreach循环是使用IEnumerable接口的主要原因之一。与经典的C风格循环相比,foreach具有非常简洁的语法,并且非常易于理解,后者需要检查各种变量以了解它正在做什么。

yield 关键字

可能较少人知道的功能是,IEnumerable还可以通过使用yield returnyield break语句来启用C#中的生成器(generators)

IEnumerable<Thing> GetThings() {
   if (isNotReady) yield break;
   while (thereIsMore)
     yield return GetOneMoreThing();
}

抽象

在实践中另一个常见的场景是使用 IEnumerable 提供极简抽象。因为它是一个微小且只读的接口,鼓励您将您的集合公开为 IEnumerable(而不是例如 List)。这样你就可以自由地更改你的实现而不会破坏你的客户端代码(例如,将 List 更改为 LinkedList)。

注意事项

需要注意的一种行为是,在流式实现中(例如从数据库逐行检索数据而不是先加载所有结果到内存中),您不能多次迭代集合。这与像 List 这样的内存中集合形成对比,后者可以多次迭代而不会出现问题。例如,ReSharper有一个用于检查 Possible multiple enumeration of IEnumerable 的代码检查

IEnumerator

另一方面,IEnumerator 是幕后接口,使得 IEnumerble-for-each-magic 起作用。严格来说,它支持显式迭代器。

var iter = list.GetEnumerator();
while (iter.MoveNext())
    Console.WriteLine(iter.Current);

根据我的经验,由于其冗长的语法和稍微令人困惑的语义(至少对我来说),IEnumerator在常见情景中很少使用(例如,MoveNext()也会返回一个值,但名称并未表明这一点)。

IEnumerator的用例

我只在特定的(稍低级别的)库和框架中使用IEnumerator,其中我提供了IEnumerable接口。一个例子是数据流处理库,它在foreach循环中提供了一系列对象,即使在幕后,数据是使用各种文件流和序列化收集的。

客户端代码

foreach(var item in feed.GetItems())
    Console.WriteLine(item);

图书馆

IEnumerable GetItems() {
    return new FeedIterator(_fileNames)
}

class FeedIterator: IEnumerable {
    IEnumerator GetEnumerator() {
        return new FeedExplicitIterator(_stream);
    }
}

class FeedExplicitIterator: IEnumerator {
    DataItem _current;

    bool MoveNext() {
        _current = ReadMoreFromStream();
        return _current != null;           
    }

    DataItem Current() {
        return _current;   
    }
}

12

IEnumerable和IEnumerator之间的区别:

  • IEnumerable在内部使用IEnumerator。
  • IEnumerable不知道正在执行哪个项/对象。
  • 每当我们将IEnumerator传递给另一个函数时,它知道项/对象的当前位置。
  • 每当我们将一个IEnumerable集合传递给另一个函数时,它不知道项/对象的当前位置(不知道它正在执行哪个项)

    IEnumerable有一个方法GetEnumerator()

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

IEnumerator有一个叫做Current的属性和两个方法,Reset()和MoveNext()(在了解列表中项目的当前位置时非常有用)。

public interface IEnumerator
{
     object Current { get; }
     bool MoveNext();
     void Reset();
}

10

实现 IEnumerable 接口意味着该对象可以被迭代。这并不一定意味着它是一个数组,因为某些列表不能被索引,但是你可以枚举它们。

IEnumerator 是用于执行迭代的实际对象。它控制从列表中的一个对象移动到下一个对象。

大多数情况下,IEnumerableIEnumerator 透明地作为 foreach 循环的一部分使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接