谁能向我解释IEnumerable和IEnumerator?[关闭]


272

谁能向我解释IEnumerable和IEnumerator?

例如,何时在foreach上使用它?IEnumerable和IEnumerator有什么区别?我们为什么需要使用它?


54
这是因为Java和JavaScript最坏命名的mixup
马修锁定

@MatthewLock可以解释您的意思吗?
David Klempfner,

Answers:


274

例如,何时在foreach上使用它?

您不使用IEnumerable“ over” foreach。实现IEnumerable使使用成为foreach 可能

当您编写如下代码时:

foreach (Foo bar in baz)
{
   ...
}

它在功能上等同于编写:

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

我所说的“功能等效”实际上是编译器将代码转换成的功能。不能使用foreachbaz在这个例子中,除非 baz器具IEnumerable

IEnumerable表示baz实现该方法

IEnumerator GetEnumerator()

IEnumerator此方法返回的对象必须实现这些方法

bool MoveNext()

Object Current()

第一种方法前进到IEnumerable创建枚举器的对象中的下一个对象,false如果完成,则返回,第二种方法返回当前对象。

.Net中可以迭代实现的任何内容IEnumerable。如果您正在构建自己的类,并且该类尚未从实现的类继承IEnumerable,则可以foreach通过实现IEnumerable(以及通过创建其新GetEnumerator方法将返回的枚举器类)使该类在语句中可用。


15
我认为当原始张贴者说“在foreach上方”时,他的意思是“何时应该显式调用GetEnumerator()/ MoveNext()而不是使用foreach循环。” 物有所值。
mqp

6
啊,我认为你是对的。好吧,答案在我的帖子中是隐含的:没关系,因为无论如何编译器都会这样做。因此,最简单的方法就是使用foreach。
罗伯特·罗斯尼

12
这个答案不能说明全部。可枚举的对象不需要实现IEnumerable;他们只需要有一个GetEnumerator方法,该方法返回一个类型的实例,该类型的实例又具有一个bool MoveNext()方法和Current任何类型的属性。在C#1.0中实现了这种“鸭子类型”方法,作为一种在枚举值类型集合时避免装箱的方法。
phoog,2012年

3
通过上面的讲解,并完成以下工作:实施(T)的IEnumerable,您将获得大量资源。
ruedi

4
这有点像C ++ iterator吗?
马特·G

161

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();
}

这样,临时对象用户将不会找到Garage的GetEnumerator()方法,而foreach构造将在必要时在后台获取接口。

改编自Pro C#5.0和.NET 4.5框架


1
很棒的答案,谢谢!我确实有一个问题。在第一个示例中,您使用foreach遍历了数组,但是在第二个示例中,您无法执行该操作。这是因为数组在类中还是因为它包含对象?
卡雷布

谢谢您的精彩解释。我唯一的问题是,为什么不只是有一个方法,Garage那得carArray?这样,您就不必实现GetEnumerator自己,因为Array已经做到了。EGforeach(Car c in carLot.getCars()) { ... }
尼克·罗兰

62

实现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”遍历其内容的类。
康坦戈

54

类比解释+代码演练

首先是没有代码的解释,然后在以后添加。

假设您经营一家航空公司。在每架飞机上,您都想知道有关在飞机上飞行的乘客的信息。基本上,您希望能够“遍历”飞机。换句话说,您希望能够从前排座位开始,然后朝着飞机的后部前进,向乘客询问一些信息:他们是谁,来自哪里等。飞机只能做到这一点, 如果是:

  1. 可数的
  2. 如果有柜台。

为什么要这些要求?因为这就是界面所需要的。

如果这是信息过载,那么您所需要知道的是,您希望能够向飞机上的每个乘客询问一些问题,从第一个问题一直到最后一个问题。

可数是什么意思?

如果一家航空公司是“可数”的,这意味着飞机上必须有一名空姐在场,唯一的工作就是计算-该空姐必须以一种非常具体的方式进行计数:

  1. 柜台/乘务员必须在第一个乘客之前开始(在每个演示安全性,如何穿上救生衣的人的前面)。
  2. 他(她)(即乘务员)必须“下一个”上走道到第一个座位。
  3. 然后他/她将记录:(i)该人在座位上,以及(ii)他们在过道中的当前位置。

盘点程序

航空公司的机长希望对每位旅客进行调查或计数时提供报告。因此,在与坐在第一位的人讲话之后,空乘人员/柜台便会向机长报告,当给出报告时,柜台会记住他/她在过道中的确切位置,并继续向右计数他/她离开的位置关。

以这种方式,机长总是能够获得有关正在调查的当前人员的信息。这样,如果他发现这个人喜欢曼彻斯特城,那么他可以给予该乘客以优惠待遇等。

  • 柜台一直走到他到达飞机尽头为止。

让我们将其与IEnumerables绑定

  • 枚举只是飞机上的乘客集合。民航法-这些基本上是所有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)知道他正在处理的当前人。

可枚举只是“可数”的花哨词。换句话说,可枚举允许您“枚举”(即计数)。


23

IEnumerable实现GetEnumerator。调用该方法时,该方法将返回一个实现MoveNext,Reset和Current 的IEnumerator

因此,当您的类实现IEnumerable时,就是说您可以调用方法(GetEnumerator)并获取返回的新对象(IEnumerator),该对象可以在foreach之类的循环中使用。


18

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

IEnumerator允许使用yield关键字,foreach样式对列表中的项目进行顺序访问。

在foreach实现之前(例如,在Java 1.4中),迭代列表的方法是从列表中获取一个枚举数,然后向其询问列表中的“下一个”项,只要返回的值是下一个返回的值即可。项目不为空。Foreach只是作为语言功能隐式地做到了这一点,就像lock()在幕后实现Monitor类的方式一样。

我希望foreach可以在列表上使用,因为它们实现了IEnumerable。


如果它是列表,为什么我不能只使用foreach?
prodev42

.Net duck键入foreach语句,但IEnumerable / IEnumerable <T>是表示您的类可以枚举的适当方法。
user7116

15
  • 实现IEnumerable的对象允许其他人(通过枚举器)访问其每个项目。
  • 实现IEnumerator的对象是进行迭代。它遍历一个可枚举的对象。

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


12

IEnumerable和IEnumerator(及其通用的IEnumerable <T>和IEnumerator <T>)是.Net Framework类Libray集合迭代器实现的基本接口。

IEnumerable是大多数代码中最常见的接口。它启用了foreach循环,生成器(认为yield),并且由于其微小的接口,它被用于创建紧密的抽象。IEnumerable取决于IEnumerator

另一方面,IEnumerator提供了一个较低级别的迭代接口。它被称为显式迭代器,它使程序员可以更好地控制迭代周期。

IEnumerable

IEnumerable是一个标准接口,允许对支持它的集合进行迭代(实际上,我今天想到的所有集合类型都实现了IEnumerable)。编译器支持允许使用语言功能,例如foreach。一般而言,它启用了此隐式迭代器实现

foreach循环

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

我认为foreach循环是使用IEnumerable接口的主要原因之一。foreach与经典的C风格的循环相比,它的语法非常简洁且易于理解,在循环中您需要检查各种变量以查看其作用。

产生关键字

IEnumerable可能是一个鲜为人知的功能还通过使用and 语句启用C#中的生成器yield returnyield break

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

抽象

实际上,另一个常见的方案是使用IEnumerable提供简约的抽象。由于这是一个很小的只读接口,因此建议您将集合公开为 IEnumerable(例如,而不是List)。这样,您可以自由地更改实现,而无需破坏客户端的代码(例如,将List更改为LinkedList)。

Gotcha

要注意的一个行为是,在流实现中(例如,从数据库中逐行检索数据,而不是先将所有结果加载到内存中),您不能多次遍历集合。这与诸如List之类的内存中集合形成对比,您可以在其中进行多次迭代而不会出现问题。例如,ReSharper 对IEnumerable的可能的多重枚举进行了代码检查

IEnumerator

另一方面,IEnumerator是使IEnumerble-foreach-magic正常工作的后台界面。严格来说,它启用显式迭代器。

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

以我的经验,由于其冗长的语法和稍微混乱的语义(至少对我来说;例如MoveNext()也返回一个值,该名称根本不建议),因此IEnumerator很少在常见情况下使用。

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;   
    }
}

9

实现IEnumerable本质上意味着可以迭代该对象。这不一定意味着它是一个数组,因为有些列表无法索引,但是您可以枚举它们。

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

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


6

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();
}

5

IEnumerable是一个包含Ienumerator的框。IEnumerable是所有集合的基本接口。如果集合实现IEnumerable,则foreach循环可以运行。在下面的代码中,它说明了拥有我们自己的枚举器的步骤。首先定义要收集的Class。

public class Customer
{
    public String Name { get; set; }
    public String City { get; set; }
    public long Mobile { get; set; }
    public double Amount { get; set; }
}

现在,我们将定义类,该类将充当类Customer的集合。注意,它正在实现IEnumerable接口。这样我们就必须实现GetEnumerator方法。这将返回我们的自定义枚举器。

public class CustomerList : IEnumerable
{
    Customer[] customers = new Customer[4];
    public CustomerList()
    {
        customers[0] = new Customer { Name = "Bijay Thapa", City = "LA", Mobile = 9841639665, Amount = 89.45 };
        customers[1] = new Customer { Name = "Jack", City = "NYC", Mobile = 9175869002, Amount = 426.00 };
        customers[2] = new Customer { Name = "Anil min", City = "Kathmandu", Mobile = 9173694005, Amount = 5896.20 };
        customers[3] = new Customer { Name = "Jim sin", City = "Delhi", Mobile = 64214556002, Amount = 596.20 };
    }

    public int Count()
    {
        return customers.Count();
    }
    public Customer this[int index]
    {
        get
        {
            return customers[index];
        }
    }
    public IEnumerator GetEnumerator()
    {
        return customers.GetEnumerator(); // we can do this but we are going to make our own Enumerator
        return new CustomerEnumerator(this);
    }
}

现在,我们将如下创建自己的自定义枚举器。因此,我们必须实现方法MoveNext。

 public class CustomerEnumerator : IEnumerator
    {
        CustomerList coll;
        Customer CurrentCustomer;
        int currentIndex;
        public CustomerEnumerator(CustomerList customerList)
        {
            coll = customerList;
            currentIndex = -1;
        }

        public object Current => CurrentCustomer;

        public bool MoveNext()
        {
            if ((currentIndex++) >= coll.Count() - 1)
                return false;
            else
                CurrentCustomer = coll[currentIndex];
            return true;
        }

        public void Reset()
        {
            // we dont have to implement this method.
        }
    }

现在,我们可以像下面这样在我们的集合上使用foreach循环;

    class EnumeratorExample
    {
        static void Main(String[] args)
        {

            CustomerList custList = new CustomerList();
            foreach (Customer cust in custList)
            {
                Console.WriteLine("Customer Name:"+cust.Name + " City Name:" + cust.City + " Mobile Number:" + cust.Amount);
            }
            Console.Read();

        }
    }

4

了解Iterator模式将对您有所帮助。我建议阅读相同。

迭代器模式

在较高的层次上,迭代器模式可用于提供一种遍历任何类型的集合的标准方式。在迭代器模式中,我们有3个参与者,实际集合(客户端),聚合器和迭代器。聚合是一个接口/抽象类,它具有返回迭代器的方法。Iterator是一个接口/抽象类,它具有允许我们迭代集合的方法。

为了实现该模式,我们首先需要实现一个迭代器以生成可以迭代所关注集合(客户端)的具体对象,然后集合(客户端)实现聚合器以返回上述迭代器的实例。

这是UML图 迭代器模式

因此,基本上在c#中,IEnumerable是抽象聚合,而IEnumerator是抽象Iterator。IEnumerable具有单个方法GetEnumerator,该方法负责创建所需类型的IEnumerator实例。像列表这样的集合实现IEnumerable。

例。假设我们有一个方法getPermutations(inputString)返回一个字符串的所有排列,并且该方法返回一个实例。IEnumerable<string>

为了计算排列的数量,我们可以执行以下操作。

 int count = 0;
        var permutations = perm.getPermutations(inputString);
        foreach (string permutation in permutations)
        {
            count++;
        }

C#编译器或多或少将以上内容转换为

using (var permutationIterator = perm.getPermutations(input).GetEnumerator())
        {
            while (permutationIterator.MoveNext())
            {
                count++;
            }
        }

如有任何疑问,请随时提出。


2

次要贡献。

正如许多人解释的那样,“何时使用”和“与foreach一起使用”。我考虑过根据有关IEnumerable和IEnumerator两者之间的区别的要求在此处添加“其他国家的区别”

我基于以下讨论线程创建了以下代码示例。

IEnumerable,IEnumerator vs foreach,何时使用 IEnumerator和IEnumerable 什么区别?

Enumerator保留函数调用之间的状态(迭代位置),而Enumerable则不进行迭代。

这是经过测试的示例,带有注释以供理解。

专家请加/改我。

static void EnumerableVsEnumeratorStateTest()
{
    IList<int> numList = new List<int>();

    numList.Add(1);
    numList.Add(2);
    numList.Add(3);
    numList.Add(4);
    numList.Add(5);
    numList.Add(6);

    Console.WriteLine("Using Enumerator - Remembers the state");
    IterateFrom1to3(numList.GetEnumerator());

    Console.WriteLine("Using Enumerable - Does not Remembers the state");
    IterateFrom1to3Eb(numList);

    Console.WriteLine("Using Enumerable - 2nd functions start from the item 1 in the collection");
}

static void IterateFrom1to3(IEnumerator<int> numColl)
{
    while (numColl.MoveNext())
    {
        Console.WriteLine(numColl.Current.ToString());

        if (numColl.Current > 3)
        {
            // This method called 3 times for 3 items (4,5,6) in the collection. 
            // It remembers the state and displays the continued values.
            IterateFrom3to6(numColl);
        }
    }
}

static void IterateFrom3to6(IEnumerator<int> numColl)
{
    while (numColl.MoveNext())
    {
        Console.WriteLine(numColl.Current.ToString());
    }
}

static void IterateFrom1to3Eb(IEnumerable<int> numColl)
{
    foreach (int num in numColl)
    {
        Console.WriteLine(num.ToString());

        if (num>= 5)
        {
            // The below method invokes for the last 2 items.
            //Since it doesnot persists the state it will displays entire collection 2 times.
            IterateFrom3to6Eb(numColl);
        }
    }
}

static void IterateFrom3to6Eb(IEnumerable<int> numColl)
{
    Console.WriteLine();
    foreach (int num in numColl)
    {
        Console.WriteLine(num.ToString());
    }
}

2

我注意到这些差异:

答:我们以不同的方式迭代列表,foreach可用于IEnumerable,而while循环可用于IEnumerator。

B.当我们从一种方法传递到另一种方法(它开始使用当前索引)时,IEnumerator可以记住当前索引,但是IEnumerable不能记住该索引,它将索引重置为开始。该视频中的更多内容https://www.youtube.com/watch?v=jd3yUjGc9M0


1

IEnumerable并且IEnumerator都是在C#中的接口。

IEnumerable是定义一个GetEnumerator()返回IEnumerator接口的单个方法的接口。

这适用于对IEnumerable可与foreach语句。

IEnumerator有两种方法,MoveNextReset。它还具有称为的属性Current

下面显示了IEnumerable和IEnumerator的实现。


0
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Enudemo
{

    class Person
    {
        string name = "";
        int roll;

        public Person(string name, int roll)
        {
            this.name = name;
            this.roll = roll;
        }

        public override string ToString()
        {
            return string.Format("Name : " + name + "\t Roll : " + roll);
        }

    }


    class Demo : IEnumerable
    {
        ArrayList list1 = new ArrayList();

        public Demo()
        {
            list1.Add(new Person("Shahriar", 332));
            list1.Add(new Person("Sujon", 333));
            list1.Add(new Person("Sumona", 334));
            list1.Add(new Person("Shakil", 335));
            list1.Add(new Person("Shruti", 336));
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
           return list1.GetEnumerator();
        }
    }



    class Program
    {
        static void Main(string[] args)
        {
            Demo d = new Demo();  // Notice here. it is simple object but for 
                                //IEnumerator you can get the collection data

            foreach (Person X in d)
            {
                Console.WriteLine(X);
            }

            Console.ReadKey();
        }
    }
}
/*
Output : 

Name : Shahriar  Roll : 332
Name : Sujon     Roll : 333
Name : Sumona    Roll : 334
Name : Shakil    Roll : 335
Name : Shruti    Roll : 336
  */
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.