C# 迭代器设计的基本原理(与 C++ 相比)
Posted
技术标签:
【中文标题】C# 迭代器设计的基本原理(与 C++ 相比)【英文标题】:Rationale of C# iterators design (comparing to C++) 【发布时间】:2010-06-14 06:49:17 【问题描述】:我找到了类似的话题:Iterators in C++ (stl) vs Java, is there a conceptual difference?
这主要是处理 Java 迭代器(类似于 C#)无法后退的问题。
所以在这里我想关注限制——在 C++ 中,迭代器不知道它的限制,你必须自己将给定的迭代器与限制进行比较。在 C# 中,迭代器了解更多——您无需与任何外部引用进行比较即可判断迭代器是否有效。
我更喜欢 C++ 方式,因为一旦有了迭代器,您就可以将任何迭代器设置为限制。换句话说,如果您只想获取几个元素而不是整个集合,则不必更改迭代器(在 C++ 中)。对我来说它更“纯粹”(清晰)。
当然,MS 在设计 C# 时就知道这一点和 C++。那么C#方式有哪些优势呢?哪种方法更强大(这会导致基于迭代器的更优雅的函数)。我错过了什么?
如果您对 C# 与 C++ 迭代器设计的限制(边界)以外的其他想法,也请回答。
注意:(以防万一)请保持讨论严格的技术性。没有 C++/C# 火焰战争。
编辑
正如 Tzaman 所说,“将限制单独表示没有任何好处,因为除了一次走一个元素外,没有其他方法可以到达那里。”然而,构建一个一次执行几个步骤的 C# 迭代器并不难,因此问题是——具有显式限制迭代器(如在 C++ 中)是否有好处?如果是 - 什么?
@乔恩,
Edit1: 假设你有一个函数 foo,它在迭代器上做一些事情(这个例子很天真!)
void foo(iter_from,iter_end) // C++ style
void foo(iter) // C# style
现在你想在除最后 10 个元素之外的所有元素上调用功能栏。
bar(iter_from,iter_end-10); // C++ style
在 C# 中(如果我没记错的话)你必须为这个迭代器提供额外的方法来改变 its 限制,像这样:
bar(iter.ChangeTheLimit(-10));
Edit2: 重读您的帖子后,我感受到了至关重要的不同。在 C++ 中,您处理集合的迭代器,在 C# 中,您处理集合(迭代器在“内部”使用)。如果是的话,我仍然对 C# 感到有点不舒服——你迭代集合,当你发现有趣的元素时,你想将所有元素从“这里”传递到结束。在 C++ 中,这非常容易,并且没有开销。在 C# 中,您可以传递一个迭代器或一个集合(如果后者将有额外的计算)。我会等你的评论:-)
@汉斯,
我不是在比较苹果和橘子。比较。理论在这里是共同点,所以你有排序算法、分区等。你有集合的概念(或序列,正如 Jon 喜欢的那样)。现在 - 问题是您如何设计对元素的访问以拥有用 C# 或 C++(或任何其他语言)编写的优雅算法。我想理解“我们这样做是因为……”的原因。
我知道 .NET 迭代器和集合是独立的类。我知道访问元素和整个集合之间的区别。然而,在 C++ 中,处理集合的最通用方法是使用迭代器——尽管这些集合完全不同,但您可以通过这种方式使用列表和向量。另一方面,在 C# 中,您更愿意编写
sort(IEnumerable<T> coll)
改为函数
sort(IEnumerator<T> iter)
正确吗?所以从这个意义上说,我猜你不能把 C# 迭代器当作 C++ 迭代器,因为 C# 并不像 C++ 那样表达相同的算法。或者正如 Jon 指出的那样——在 C# 中,您宁愿转换集合(Skip、Take)而不是更改迭代器。
【问题讨论】:
@macias,因为既不是 C++ 也不是 C# 开发人员,我更喜欢您示例中的 C# 风格。对我来说它看起来更自然。 C++ 中如何处理无限序列?在这种情况下 iter_end 会是什么? @Damien,我从未在 C++ 中使用过不定式序列。 两个字:责任原则。 @Damien:对于无限序列,iter_end
必须是一个永远不会达到的特殊值(例如,如果流永远不会结束,istream_iterator 可以表示无限范围)。但是,如果一种算法被设计为使用无限序列,它可能只需要一个迭代器。例如,算法std::copy
采用两个输入迭代器来表示源范围(或序列),但只有一个输出迭代器,它或多或少可以看作是一个无限的复制范围。 [...]
【参考方案1】:
在我看来,C# 设计更加封装:一个序列用完或执行,独立于其他任何东西。将一个限制与另一个限制进行比较有意义在哪里?
如果您只想获取几个元素,那么 LINQ 提供了任意数量的方法来从另一个构建一个序列,例如
foo.Take(10)
foo.Skip(10)
foo.Where(x => x.StartsWith("y"))
等
我认为将一个序列转换为另一个序列比用限制指定它更清晰 - 也更可组合。如果您想将迭代器传递给另一个函数,但又想将其限制为前几个元素,那么为什么必须也传递限制?为什么不直接传递具有自限性的变换序列呢?
编辑:要解决您的问题,请编辑:在 C# 中(至少使用 LINQ),您不会修改现有集合。您将从旧序列创建一个新序列。这是懒惰地完成的;它不会创建新副本或类似的东西。对于 LINQ to Objects,这是使用 IEnumerable<T>
上的扩展方法执行的,因此任何序列都可以获得相同的功能。
请注意,这不仅限于传统的集合 - 它可以是从日志文件中读取的一系列行(同样,懒惰)。您不需要任何关于集合本身的知识,只需要它是一个可以从中绘制项目的序列。 IEnumerable<T>
和 IEnumerator<T>
之间也有区别,其中第一个表示序列,第二个表示序列上的迭代器; IEnumerator<T>
在 C# 中很少显式使用或传递。
现在,您的“除最后 10 个元素之外的所有元素”的示例是一个棘手的示例,因为对于一般序列,您无法判断您是从末尾到末尾的 10 个元素。 LINQ to Objects 中没有任何内容可以显式执行此操作。对于任何实现 ICollection
或 ICollection<T>
的东西,您都可以使用
Bar(list.Take(list.Count - 10))
但这不是很笼统。一个更通用的解决方案需要维护一个包含 10 个元素的循环缓冲区,有效地在它产生的位置之前读取 10 个元素。老实说,我很少发现这是一项要求。
【讨论】:
是的...这确实看起来更好,但他们希望我们在 LINQ 之前做什么? 感谢您的回答。在您的示例和下面的示例中,您假设您的“输入”是一个集合。但是如果它已经是一个迭代器呢?由于 cmets 格式不好,我编辑了我的帖子。 @Mark:在 C# 1 中,这样做会非常难看。在 C# 2 中,您可以使用迭代器块相当容易地构建自己的伪 LINQ - 但是它会缺乏 lambda 表达式和扩展方法的简洁性。 @Jon,我知道这不是关于修改集合元素 :-) 现在,关于这个例子——你不能依赖于你免费获得 Count 的事实,例如对于链表它是 O( n) 得到它,所以在这种情况下 C++ 会快得多。此外,您再次使用 collection 而不是 iterator。所以我越来越好奇——如果真的 C++ 迭代器是 C# 集合(不是迭代器)的对应物?或者,也许我改写我的问题——C++ STL 通用函数(#include )适用于迭代器,而 C# STL 将适用于 collections,对吗? @macias:您在 .NETLinkedList<T>
... 中获得 O(1) 的计数...并且您还假设您可以轻松地在链接列表中到达“从末尾开始的 10” ,这将要求它是一个双向链表。但是,是的,我承认在不维护计数的双向链表的情况下,在 .NET 中获取除最终 X 元素之外的所有元素更棘手。不过,我不能说我遇到过这种确切的情况 :) 就集合与迭代器而言:更准确的说法是 .NET API 通常根据 sequences 而不是集合工作.并非每个序列都是正常的集合。【参考方案2】:
我认为,只要您只谈论 forward 迭代器,我就喜欢 C# 方式 - 序列知道自己的限制,您可以通过 LINQ 轻松转换它,等等。单独表示限制没有任何好处,因为除了一次走一个元素之外,没有办法到达。
但是,C++ 迭代器的功能要强大得多——您可以使用双向迭代器或随机访问迭代器,它可以让您完成单向顺序迭代器无法完成的事情。顺序迭代器在某种意义上是链表的泛化,而随机访问迭代器是数组的泛化。这就是为什么您可以用 C++ 迭代器有效地表达诸如快速排序之类的算法,而使用 IEnumerable
则不可能。
【讨论】:
哦,我喜欢你的回答——但是你能否解释一下最后的陈述。或者发布一个链接到一些分机。参考。关于各种迭代器——一个不否认另一个,即你可以有 C# 迭代器(有限制),但双向。 当然,对于“随机访问迭代器”,您只需使用IList<T>
。我在这里 +1 的原因是双向迭代器的想法,.NET 不支持。我不能说这是我非常想要的东西,但这绝对是一个有趣的遗漏。
@Jon - 当然; IList<T>
仍然是(用 macias 的术语)collection 本身的表示,而不是该集合中的 position。我认为 C++ 迭代器概念是向集合公开通用接口的最佳方式,而无需语言中的实际接口 - 绝对比强制 ABC 继承要好得多,只是为了能够使用 STL algorithm
/ 等等。任何猜测至于为什么没有.NET Deque<T>
/ IBidirectional
?它们是有用的......
@tzman:在许多情况下,您可以将LinkedList<T>
用作Deque<T>
,尽管这会带来内存成本。我不知道为什么没有循环缓冲区实现,除了实现 anything 需要时间......我怀疑这个价值还不够大。
有什么理由为什么IEnumerator<T>
不应该支持int Move(int);
方法,对于非负值,它会尝试调用MoveNext
指定的次数并返回0,如果它可以移动全部数量,或者如果不能移动不足的数量(如果连接多个IEnumerable<T>
集合,后一个数量很有用)?如果不出意外,任何IEnumerator<T>
实现都可以实现调用MoveNext()
的方法,但是许多实现可以使用比重复调用MoveNext
快几个数量级的替代方法。【参考方案3】:
你在比较苹果和橘子。 STL 集合类有完全不同的设计目标。它们是集合的一刀切设计,您只能获得所提供的类。无法自定义集合,STL 类并非旨在继承自。
与.NET 非常不同,它们的核心是ICollection、IDictionary 和IList 接口。 .NET 框架包含数百个集合类,经过定制以完成其特定工作。
这也影响了他们的迭代器的设计。 STL 迭代器必须提供很大的灵活性,因为它们迭代的集合无法自定义。有一个与此相关的固有成本。有 5 种不同的 STL 迭代器类型,并非所有集合类都支持所有 5 种不同的类型。在库设计中,失去通用性从来都不是一件好事。
虽然可以说 C++ 迭代器更灵活,但我会做出相反的断言。 IEnumerator 的简单性促成了非常有价值的 .NET 功能。例如,C# 的 yield 关键字将集合与迭代器的行为完全分离。如果不是简单的迭代器类型,Linq 就不会发生。协方差支持是不可能的。
关于您的 Edit2,不,.NET 迭代器是独立的类,就像 C++ 的一样。请务必了解 IEnumerable(由集合实现)和 IEnumerator(由迭代器实现)之间的区别。
【讨论】:
好的,有很多内容需要编辑 :-) 我想你通过 yield 示例让我信服了。 IE。使用两个迭代器来定义边界,您必须提前知道极限(极限可以改变,但它必须在那里)。所以 yield 对应每次都必须传输两个值,这意味着当前迭代器必须知道它的限制 --> 这导致我们使用 C# 迭代器。不错。 我恭敬地认为这是一个错误的答案。首先,您实际上对 STL 集合和迭代器是不正确的:它们当然可以被继承和自定义。 STL 旨在将算法与结构分开——也就是说,std::copy
或std::for_each
函数不必关心它的参数指向什么,只要它们是有效的迭代器即可。你对 C# 也有错误:没有迭代器,只有集合。
好吧,好吧。我应该澄清一下: IEnumerable 和 IEnumerableGetEnumerator
的方法,它返回一个实现 IEnumerator
的对象,它公开了一个名为 MoveNext
的方法。然而,这个枚举器与迭代器并不是一回事。那么C#方式的优点是什么?
封装一个。如果您不手动设置迭代顺序,则更难弄乱您的迭代限制。
C++ 示例:
std::vector<int> a;
std::vector<int> b;
std::vector<int>::iterator ba = a.begin();
std::vector<int>::iterator ea = a.end();
std::vector<int>::iterator bb = b.begin();
std::vector<int>::iterator eb = b.end();
// lots of code here
for(auto itr = ba; itr != eb; itr++) // iterator tries to "cross" vectors
// you get undefined behavior, depending
// on what you do with itr
哪种方法更强大(哪种 导致基于更优雅的功能 在迭代器上)。
这取决于你所说的最强大是什么意思。
C++ 方法更灵活。
C# 更难误用。
【讨论】:
你的例子很有说服力,谢谢!强大 = 您可以以清晰的方式表达大多数算法(排序、分区、查找、任何、所有等)。 @macias:您也可以使用 C# 序列表达分区、查找、任何和所有(使用 lambda 表达式非常容易)。 对于这个定义,C++ 迭代器肯定更强大。你有 i/o 迭代器,为算法指定自定义范围(`std::sort(c.begin(), c.begin()+3)`),使用与迭代器相同的算法和原始指针 i>) 等等。【参考方案5】:由于以下问题,我找到了这个主题。有一个序列需要一次向前迭代一个元素。但是,在某些点需要跳过大量元素。当发生这种跳过时,除了在新索引处跳转和重新启动之外,其他任何操作都是低效的(即,即使没有执行任何循环体,也不能为跳过的索引运行循环。)
这是一个 C# sn-p 给你的想法,除了它使用的是 int 索引而不是枚举器。
for(int i = 0; i< 1000; ++i)
... // do something
if (i == 100)
i = 200; // skip from 100 to 200
关键是可以针对这个特定的序列优化进入下一个元素,但是跳过元素(以及完全随机访问)的成本更高。 @Jon,因此为此目的使用 IList 效率低下。 Yield 也不直接允许这种风格。
我在 C# 中尝试了各种方法来执行此操作,包括对内部块使用 lambda ForEach() 样式,它返回下一个索引。但是,这不是很好。
可能导致开销最小的方法是简单的迭代器类型——不基于 IEnumerable 或 yield——它允许我模仿上面的代码。
想法?
【讨论】:
以上是关于C# 迭代器设计的基本原理(与 C++ 相比)的主要内容,如果未能解决你的问题,请参考以下文章
C++ 的STL中,迭代器那个指针为何++能指向下一个元素?