为什么我们需要两个接口来枚举集合?



我一直在试图理解IEnumerableIEnumerator背后的想法已经有一段时间了。我阅读了我在网上可以找到的所有问题和答案,特别是在StackOverflow上,但我并不满意。我明白应该如何使用这些接口,但不知道为什么以这种方式使用它们。

我认为我误解的本质是我们需要两个接口来进行一个操作。我意识到,如果两者都需要,一个可能还不够。所以我采用了相当于foreach的"硬编码"(如我在这里找到的):

while (enumerator.MoveNext())
{
object item = enumerator.Current;
// logic
}

并试图让它与一个接口一起工作,认为会出错,这将使我理解为什么需要另一个接口。

所以我创建了一个集合类,并实现了IForeachable

class Collection : IForeachable
{
private int[] array = { 1, 2, 3, 4, 5 };
private int index = -1;
public int Current => array[index];
public bool MoveNext()
{
if (index < array.Length - 1)
{
index++;
return true;
}
index = -1;
return false;
}
}

并使用等效foreach来提名集合:

var collection = new Collection();
while (collection.MoveNext())
{
object item = collection.Current;
Console.WriteLine(item);
}

它有效!那么这里缺少什么,需要另一个接口呢?

谢谢。


编辑:我的问题不是评论中列出的问题的重复:

  • 这个问题就是为什么首先需要接口来枚举。
  • 这个问题
  • 和这个问题是关于这些接口是什么以及应该如何使用它们。

我的问题是,为什么它们是这样设计的,而不是它们是什么,它们是如何工作的,以及为什么我们首先需要它们。

这两个接口是什么,它们有什么作用?

IEnumerable 接口放置在集合对象上并定义 GetEnumerator() 方法,这将返回一个实现 IEnumerator 接口的(通常是新的)对象。C# 中的 foreach 语句和 VB.NET 中的 For Each 语句使用 IEnumerable 访问枚举器,以便循环访问集合中的元素。

IEnumerator 接口本质上是放置在实际执行迭代的对象上的协定。它存储迭代的状态,并在代码在集合中移动时更新它。

为什么不让集合也成为枚举器?为什么有两个独立的接口?

没有什么可以阻止IEnumerator和IEnumerable在同一类上实现。但是,这样做会受到惩罚 - 不可能同时在同一集合上有两个或多个循环。如果可以绝对保证不需要同时循环两次集合,那很好。但在大多数情况下,这是不可能的。

什么时候有人会一次多次迭代一个集合?

这里有两个例子。

第一个示例是在同一集合上有两个相互嵌套的循环。如果集合也是枚举器,则不可能在同一集合上支持嵌套循环,当代码到达内部循环时,它将与外部循环发生冲突。

第二个示例是当有两个或多个线程访问同一集合时。同样,如果集合也是枚举器,则无法在同一集合上支持安全的多线程迭代。当第二个线程尝试遍历集合中的元素时,两个枚举的状态将发生冲突。

此外,由于 .NET 中使用的迭代模型不允许在枚举期间更改集合,因此这些操作在其他方面是完全安全的。

——这是我多年前写的一篇博客文章:https://colinmackay.scot/2007/06/24/iteration-in-net-with-ienumerable-and-ienumerator/

您的IForeachable甚至不能从两个不同的线程迭代(您根本不能有多个活动迭代 - 即使来自同一个线程),因为当前枚举状态存储在IForeachable本身中。每次完成枚举时,您还必须重置当前位置,如果您忘记这样做 - 好吧,下一个调用者会认为您的集合是空的。我只能想象这一切可能导致的各种难以跟踪的错误。

另一方面,由于IEnumerable为每个调用方返回新IEnumerator- 可以同时进行多个枚举,因为每个调用方都有自己的枚举状态。我认为仅这个原因就足以证明两个接口的合理性。枚举本质上是读取操作,如果不能在多个位置同时读取相同内容,那将非常混乱。

最新更新