【问题标题】:Would C# benefit from distinctions between kinds of enumerators, like C++ iterators?C# 是否会受益于枚举器类型之间的区别,例如 C++ 迭代器?
【发布时间】:2009-10-28 02:40:21
【问题描述】:

我一直在思考IEnumerator.Reset() 方法。我在 MSDN 文档中读到它仅用于 COM 互操作。作为一名 C++ 程序员,在我看来,支持ResetIEnumerator 就是我所说的forward iterator,而不支持ResetIEnumerator 实际上是input iterator

所以我的问题之一是,这种理解是否正确?

我的问题的第二部分是,如果在输入迭代器和前向迭代器(或“枚举器”,如果您愿意的话)之间存在区别,在 C# 中会有什么好处吗?是否有助于消除程序员之间的一些困惑,就像在SO question about cloning iterators 中发现的那样?

编辑:澄清转发和输入迭代器。输入迭代器仅保证您只能枚举集合的成员(或来自生成器函数或输入流)一次。这正是 IEnumerator 在 C# 中的工作方式。是否可以枚举第二次,取决于是否支持Reset。前向迭代器没有这个限制。您可以根据需要多次枚举成员。

一些 C# 程序员不理解为什么 IEnumerator 不能可靠地用于多通道算法中。考虑以下情况:

void PrintContents(IEnumerator<int> xs)
{
  while (iter.MoveNext())
    Console.WriteLine(iter.Current); 
  iter.Reset();
  while (iter.MoveNext())
    Console.WriteLine(iter.Current); 
}

如果我们在这种情况下调用PrintContents,没问题:

List<int> ys = new List<int>() { 1, 2, 3 }
PrintContents(ys.GetEnumerator()); 

但是请看以下内容:

IEnumerable<int> GenerateInts() {   
  System.Random rnd = new System.Random();
  for (int i=0; i < 10; ++i)
    yield return Rnd.Next();
}

PrintContents(GenerateInts());

如果IEnumerator 支持Reset,换句话说,支持多遍算法,那么每次迭代集合都会有所不同。这将是不可取的,因为这将是令人惊讶的行为。这个例子有点假,但它确实发生在现实世界中(例如从文件流中读取)。

【问题讨论】:

  • 我想你的意思是IEnumerator.Reset,而不是IEnumerable&lt;T&gt;.Reset,对吧?
  • 好的,谢谢!对此感到抱歉。
  • 有趣的问题。但也许您应该稍微解释一下 C++ 语言,因为很多 C# 程序员都会看到这一点。输入迭代器和前向迭代器究竟是什么可能并不明显(特别是它们的单通道/多通道功能,这与这个问题真正相关)

标签: c# c++ ienumerable iterator


【解决方案1】:

Reset 是个大错误。我在Reset 上打电话给恶作剧。在我看来,反映您在 .NET 类型系统中对“前向迭代器”和“输入迭代器”所做的区分的正确方法是区分 IEnumerable&lt;T&gt;IEnumerator&lt;T&gt;

另请参阅this answer,其中 Microsoft 的 Eric Lippert(毫无疑问,以非官方身份,我的观点只是他的资历比我必须声称这是一个设计错误的资格更多)提出了类似的观点在厘米。另请参阅his awesome blog

【讨论】:

【解决方案2】:

有趣的问题。我的看法是 C# 当然会受益。但是,添加起来并不容易。

C++ 的区别在于其更灵活的类型系统。在 C# 中,您没有一种强大的通用方法来克隆对象,这是表示前向迭代器所必需的(以支持多遍迭代)。当然,为了真正有用,您还需要支持双向和随机访问迭代器/枚举器。为了让它们都能顺利运行,你真的需要某种形式的鸭子类型,就像 C++ 模板一样。

最终,这两个概念的范围是不同的。

在 C++ 中,迭代器应该代表您需要了解的有关一系列值的所有信息。给定一对迭代器,我不需要 原始容器。我可以排序,我可以搜索,我可以随心所欲地操纵和复制元素。原始容器不在图片中。

在 C# 中,枚举器的作用并不大。最终,它们只是为了让您以线性方式运行序列。

至于Reset(),人们普遍认为一开始添加它是一个错误。如果它有效并且被正确实现,那么是的,您可以说您的枚举器类似于转发迭代器,但一般来说,最好将其视为错误而忽略。然后所有的枚举器都类似于输入迭代器。

很遗憾。

【讨论】:

  • 喝完一杯咖啡后,我暂时不明白为什么多次迭代器需要克隆(忽略前向迭代器的全部要求)。
  • 你如何在前向迭代器上进行多次传递?它不是双向的,因此您无法回到原来的位置并再次迭代。您必须创建迭代器的副本,因此您有两个迭代器指向序列中的相同位置,然后它们可以单独递增。
  • 如果您认为Reset() 方法可以取代克隆,是的,有点。除了重置只会让你回到序列中的一个固定点(开始)。但是前向迭代器应该能够对数据的子集进行多次传递(例如,仅最后三个元素)。重置在那里并没有真正的帮助。 (或者至少,如果您必须进行完全重置并遍历整个序列只是为了回到您想要执行多遍遍历的位置,那将会非常缓慢)
  • 添加枚举数的区别仅在极少数用例中有用。代价是将复杂性引入所有用例。 IMO 将 C++ 迭代复杂性排除在 C# 之外并保持简单是一个很好的决定(无论是否有意)。
【解决方案3】:

从 C# 的角度来看:

您几乎从不直接使用IEnumerator。通常你会做一个foreach 语句,它需要一个IEnumerable

IEnumerable _myCollection;
...
foreach (var item in _myCollection) { /* Do something */ }

IEnumerator 你也不会四处走动。如果你想传递一个需要迭代的集合,你传递IEnumerable。由于IEnumerable 有一个函数,它返回一个IEnumerator,它可以用于多次迭代集合(多次传递)。

IEnumerator 上不需要 Reset() 函数,因为如果你想重新开始,你只需扔掉旧的(垃圾收集)并获得一个新的。

【讨论】:

    【解决方案4】:

    如果有一种方法可以向IEnumerator&lt;T&gt; 询问它可以支持哪些能力以及它可以做出哪些承诺,那么 .NET 框架将受益匪浅。这些功能在IEnumerable&lt;T&gt; 中也很有帮助,但是能够询问枚举器的问题将允许可以从包装器(如ReadOnlyCollection)接收枚举器的代码以改进的方式使用底层集合,而无需涉及包装器。

    给定一个集合的任何枚举器,它能够被完整地枚举并且不是太大,可以从它产生一个IEnumerable&lt;T&gt;,它总是会产生相同的项目序列(特别是剩余的项目集在枚举器中)通过将其全部内容读取到数组,处理和丢弃枚举器,并从数组中获取枚举器(使用它代替原来的废弃枚举器),将数组包装在 ReadOnlyCollection&lt;T&gt; 中,然后返回.尽管这种方法适用于满足上述标准的任何类型的可枚举集合,但对于它们中的大多数来说,它的效率会非常低。有一种方法可以要求枚举器在不可变的IEnumerable&lt;T&gt; 中产生其剩余内容,这将允许多种枚举器更有效地执行指示的操作。

    【讨论】:

      【解决方案5】:

      我不这么认为。我会调用IEnumerable 一个前向迭代器和一个输入迭代器。它不允许您倒退或修改基础集合。随着foreach 关键字的添加,迭代器在大多数情况下几乎是不可想象的。

      意见: 输入迭代器(获取每个迭代器)与输出迭代器(对每个迭代器做某事)之间的区别太微不足道了,不足以证明添加到框架的合理性。此外,为了执行输出迭代器,您需要将委托传递给迭代器。输入迭代器对 C# 程序员来说似乎更自然。

      如果程序员想要随机访问,还有IList&lt;T&gt;

      【讨论】:

      • 在 C++ 中,前向迭代器支持多遍算法,这不是您可以使用 IEnumerable/IEnumerator 可靠地完成的事情。例如:如果 IEnumerable 是从 yield 函数生成的。
      • 我不明白为什么 IEnumerable 不支持多遍算法。自定义迭代器(使用 yield return)创建 IEnumerator 的实例。您可以让多个 IEnumerator 对象一次迭代同一个集合。
      • 但是给定一个 IEnumerator,你只能对集合进行一次遍历。您需要访问底层集合才能执行多遍迭代,这是一个缺点。 (因为在 C++ 中,迭代器旨在满足您的所有迭代需求。在 C# 中,一些相当基本的算法(任何需要不止一次通过的算法)必须打破抽象并需要访问底层容器)
      • 也仅供参考(这可能应该在问题中进行解释,因为很多非 C++ 程序员都会阅读它):用 C++ 的说法,一个 输入迭代器 i> 是一个支持单次遍历的迭代器,并提供不可变的值。但真正重要的是单通道方面。您基本上可以将其视为流包装器。它不支持“复制迭代器状态”的概念,所以多通道是不可能的。可以复制前向迭代器,因此它可以执行多次传递。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-10-31
      • 2016-09-07
      • 2016-01-02
      • 1970-01-01
      • 2017-05-22
      • 2013-10-27
      相关资源
      最近更新 更多