创建IEnumerator的原因是什么?

时间:2014-09-09 11:51:55

标签: c# design-patterns

IEnumerator包含MoveNext()Reset()Current作为其成员。现在假设我已将这些方法和属性移至IEnumerable接口并移除了GetEnumerator()方法和IEnumerator接口。

现在,实现IEnumerable的类的对象将能够访问方法和属性,因此可以迭代。

  • 为什么没有遵循上述方法以及我将面临的问题 如果我遵循它?
  • IEnumerator界面的存在如何解决这些问题?

5 个答案:

答案 0 :(得分:49)

迭代器包含与集合分离的状态:它包含一个光标,用于集合中的位置。因此,必须有一个单独的对象来表示额外的状态,一种获取该对象的方式,以及 on 该对象的操作 - 因此IEnumerator(和IEnumerator<T>),GetEnumerator()和迭代器成员。

想象一下,如果我们没有拥有单独的状态,那么我们写道:

var list = new List<int> { 1, 2, 3 };

foreach (var x in list)
{
    foreach (var y in list)
    {
         Console.WriteLine("{0} {1}", x, y);
    }
}

应该打印“1 1”,“1 2”,“1 3”,“2 1”等...但没有任何额外的状态,它怎么能“知道”这两个两个循环的不同位置?

答案 1 :(得分:25)

  

现在假设我已将这些方法和属性移动到IEnumerable接口并删除了GetEnumerator()方法和IEnumerator接口。

这样的设计会阻止集合中的并发枚举。如果集合本身跟踪当前位置,则不能有多个线程枚举相同的集合,甚至嵌套枚举,例如:

foreach (var x in collection)
{
    foreach (var y in collection)
    {
        Console.WriteLine("{0} {1}", x, y);
    }
}

通过将跟踪当前位置的责任委托给另一个对象(枚举器),它使集合的每个枚举独立于其他对象

答案 2 :(得分:8)

有点长的回答,之前的两个答案涵盖了大部分内容,但我在C#语言规范中查找foreach时发现了一些我觉得有趣的方面。除非您对此感兴趣,否则请停止阅读。

现在转到intering部分,根据以下陈述的C#规范扩展:

foreach (V v in x) embedded-statement

祝你:

{`
 E e = ((C)(x)).GetEnumerator();
 try {
    while (e.MoveNext()) {
        V v = (V)(T)e.Current;
        embedded-statement
    }
}
 finally {
    … // Dispose e
 }
}

x == ((C)(x)).GetEnumerator()之后使用某种身份函数(它是它自己的枚举器)并使用@ JonSkeet的循环产生类似这样的东西(为简洁起见,删除了try / catch):

var list = new List<int> { 1, 2, 3 };

while (list.MoveNext()) {
 int x = list.Current; // x is always 1
 while (list.MoveNext()) {
   int y = list.Current; // y becomes 2, then 3
   Console.WriteLine("{0} {1}", x, y);
 }
}

将打印出以下内容:

1 2
1 3

然后list.MoveNext()将永远返回false。如果你看一下这一点非常重要:

var list = new List<int> { 1, 2, 3 };

// loop
foreach (var x in list) Console.WriteLine(x); // Will print 1,2,3
// loop again
// Will never enter loop, since reset wasn't called and MoveNext() returns false
foreach (var y in list) Console.WriteLine(x); // Print nothing

所以考虑到上述情况,并注意它完全可行,因为foreach语句在检查类型是否实现GetEnumerator()之前查找IEnumerable<T> - 方法:

  

为什么没有遵循上述方法以及我将面临的问题   如果我遵循它?

您不能嵌套循环,也不能使用foreach语句多次访问同一个集合而不在它们之间手动调用Reset()。当我们在每个foreach之后处理枚举器时会发生什么?

  

IEnumerator接口的存在如何解决这些问题?

所有迭代都是相互独立的,无论我们是在讨论嵌套,多线程等,枚举都与集合本身是分开的。您可以将其视为有点像separations of concerns, or SoC,因为想法是将遍历与实际列表本身分开,并且在任何情况下遍历都不应该改变集合的状态。 IE通过您的示例调用MoveNext()将修改集合。

答案 3 :(得分:3)

其他人已经回答了你的问题,但我想补充一点细节。

让我们反编译System.Collection.Generics.List(Of T)。它的定义如下:

public class List<T> : IList<T>, ICollection<T>, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable

及其枚举器定义如下所示:

public struct Enumerator : IEnumerator<T>, IDisposable, IEnumerator

如您所见,列表本身是一个类,但其枚举器是一个结构,而此设计helps boost performance。 让我们假设您没有IEnumerableIEnumerator之间的分隔。在这种情况下,你被迫使列表成为一个结构,但this is not a very good idea,所以你不能这样做。因此,你正在失去一个获得性能提升的好机会。

IEnumerableIEnumerator分开后,您可以根据需要实现每个界面,并使用struct作为枚举器。

答案 4 :(得分:2)

迭代逻辑(foreach)未绑定到IEnumerables或IEnumerator。你需要foreach工作的是类中的一个名为GetEnumerator的方法,它返回一个具有MoveNext(),Reset()方法和Current属性的类对象。例如,以下代码有效,它将创建一个无限循环。

在设计透视图中,分离是为了确保容器(IEnumerable)在迭代(foreach)操作完成期间和之后不会保持任何状态。

    public class Iterator 
    {
        public bool MoveNext()
        {
            return true;
        }

        public void Reset()
        {

        }

        public object Current { get; private set; }
    }

    public class Tester
    {
        public Iterator GetEnumerator()
        {
            return new Iterator();
        }

        public static void Loop() 
        {
           Tester tester = new Tester();
           foreach (var v in tester)
           {
              Console.WriteLine(v);
           }

        }

    }