为什么不能克隆IEnumerator?

在C#中实现一个基本的Scheme解释器时,我惊恐地发现了以下问题:

IEnumerator没有克隆方法! (或者更准确地说,IEnumerable不能为我提供“可克隆”枚举器)。

我想要的是什么:

interface IEnumerator { bool MoveNext(); T Current { get; } void Reset(); // NEW! IEnumerator Clone(); } 

我无法想出IEnumerable的实现,它无法提供有效的可复制IEnumerator(向量,链表等等)所有能够提供IEnumerator的Clone()的简单实现,如上所述……它会比提供Reset()方法更容易!)。

缺少克隆方法意味着枚举序列的任何function/递归习惯用法都不起作用。

这也意味着我无法“无缝地”使IEnumerable的行为像Lisp“列表”(你使用car / cdr递归枚举)。 即唯一的实现“(cdr some IEnumerable )”将是非常低效的。

任何人都可以建议一个现实的,有用的IEnumerable对象的例子,它无法提供有效的“Clone()”方法吗? 是否存在“收益”构造的问题?

有人可以建议一个解决方法吗?

逻辑是不可阻挡的! IEnumerable不支持Clone ,你需要Clone ,所以你不应该使用IEnumerable

或者更准确地说,您不应该将它作为Scheme解释器工作的基础。 为什么不做一个简单的不可变链表呢?

 public class Link { private readonly TValue value; private readonly Link next; public Link(TValue value, Link next) { this.value = value; this.next = next; } public TValue Value { get { return value; } } public Link Next { get { return next; } } public IEnumerable ToEnumerable() { for (Link v = this; v != null; v = v.next) yield return v.value; } } 

请注意, ToEnumerable方法使您可以方便地使用标准C#方式。

回答你的问题:

任何人都可以建议一个现实的,有用的IEnumerable对象的例子,它无法提供有效的“Clone()”方法吗? 是否存在“收益”构造的问题?

IEnumerable可以在世界上的任何地方获取其数据。 这是一个从控制台读取行的示例:

 IEnumerable GetConsoleLines() { for (; ;) yield return Console.ReadLine(); } 

这有两个问题:首先, Clone函数编写起来并不是特别简单(并且Reset将毫无意义)。 其次,序列是无限的 – 这是完全允许的。 序列是懒惰的。

另一个例子:

 IEnumerable GetIntegers() { for (int n = 0; ; n++) yield return n; } 

对于这两个例子,你接受的“解决方法”没什么用处,因为它只会耗尽可用内存或永远挂断。 但这些都是序列的完全有效的例子。

要了解C#和F#序列,您需要查看Haskell中的列表,而不是Scheme中的列表。

如果您认为无限的东西是红鲱鱼,那么从套接字读取字节怎么样:

 IEnumerable GetSocketBytes(Socket s) { byte[] buffer = new bytes[100]; for (;;) { int r = s.Receive(buffer); if (r == 0) yield break; for (int n = 0; n < r; n++) yield return buffer[n]; } } 

如果在套接字上发送了一些字节数,则不会是无限序列。 然而,为它编写克隆将是非常困难的。 编译器如何生成IEnumerable实现以自动执行?

一旦创建了克隆,两个实例现在都必须使用它们共享的缓冲系统。 这是可能的,但实际上并不需要 - 这不是如何设计使用这些序列的方式。 你纯粹在“function上”对待它们,比如值,递归地应用filter,而不是“命令性地”记住序列中的位置。 它比低级别的car / cdr操作更清洁。

进一步问题:

我想知道,我需要的最低级别的“原语”是什么,我可能想要在我的Scheme解释器中使用IEnumerable做任何事情都可以在方案而不是内置中实现。

我认为简短的回答是关注Abelson和Sussman ,特别是关于溪流的部分 。 IEnumerable是一个流,而不是一个列表。 它们描述了如何使用特殊版本的map,filter,accumulate等来处理它们。 他们还在第4.2节中提出了统一列表和流的想法。

作为一种解决方法,您可以轻松地为进行克隆的IEnumerator创建扩展方法。 只需从枚举器创建一个列表,并将这些元素用作成员。

但是你失去了枚举器的流function – 因为你是新的“克隆”会导致第一个枚举器完全评估。

如果你可以让原始的调查员去,即。 不再使用它,您可以实现一个“克隆”function,它接受原始枚举器,并将其用作一个或多个枚举器的源。

换句话说,你可以构建这样的东西:

 IEnumerable original = GetOriginalEnumerable(); IEnumerator[] newOnes = original.GetEnumerator().AlmostClone(2); ^- extension method produce 2 new enumerators 

这些可以在内部共享原始枚举器和链接列表,以跟踪枚举值。

这将允许:

  • 无限序列,只要两个枚举器都向前推进(链表就会写入,一旦两个枚举器都通过了一个特定的点,就可以进行GC)
  • 惰性枚举,两个枚举器中的第一个,它们需要一个尚未从原始枚举器中检索的值,它将获取它并将其存储到链表中,然后再生成它

这里的问题当然是,如果其中一个调查员远远超过另一个,那么它仍然需要大量的内存。

这是源代码。 如果使用Subversion,则可以下载带有以下代码的类库的Visual Studio 2008解决方案文件,以及单独的unit testing项目。

存储库: http : //vkarlsen.serveftp.com : 81 / svnStackOverflow / SO847655
用户名和密码都是“访客”,没有引号。

请注意,此代码根本不是线程安全的。

 public static class EnumeratorExtensions { ///  /// "Clones" the specified  by wrapping it inside N new ///  instances, each can be advanced separately. /// See remarks for more information. ///  ///  /// The type of elements the  produces. ///  ///  /// The  to "clone". ///  ///  /// The number of "clones" to produce. ///  ///  /// An array of "cloned"  instances. ///  ///  /// The cloning process works by producing N new  instances. /// Each  instance can be advanced separately, over the same /// items. /// The original  will be lazily evaluated on demand. /// If one enumerator advances far beyond the others, the items it has produced will be kept /// in memory until all cloned enumerators advanced past them, or they are disposed of. ///  ///  ///  is null. ///  ///  ///  is less than 2. ///  public static IEnumerator[] Clone(this IEnumerator enumerator, Int32 clones) { #region Parameter Validation if (Object.ReferenceEquals(null, enumerator)) throw new ArgumentNullException("enumerator"); if (clones < 2) throw new ArgumentOutOfRangeException("clones"); #endregion ClonedEnumerator.EnumeratorWrapper wrapper = new ClonedEnumerator.EnumeratorWrapper { Enumerator = enumerator, Clones = clones }; ClonedEnumerator.Node node = new ClonedEnumerator.Node { Value = enumerator.Current, Next = null }; IEnumerator[] result = new IEnumerator[clones]; for (Int32 index = 0; index < clones; index++) result[index] = new ClonedEnumerator(wrapper, node); return result; } } internal class ClonedEnumerator : IEnumerator, IDisposable { public class EnumeratorWrapper { public Int32 Clones { get; set; } public IEnumerator Enumerator { get; set; } } public class Node { public T Value { get; set; } public Node Next { get; set; } } private Node _Node; private EnumeratorWrapper _Enumerator; public ClonedEnumerator(EnumeratorWrapper enumerator, Node firstNode) { _Enumerator = enumerator; _Node = firstNode; } public void Dispose() { _Enumerator.Clones--; if (_Enumerator.Clones == 0) { _Enumerator.Enumerator.Dispose(); _Enumerator.Enumerator = null; } } public T Current { get { return _Node.Value; } } Object System.Collections.IEnumerator.Current { get { return Current; } } public Boolean MoveNext() { if (_Node.Next != null) { _Node = _Node.Next; return true; } if (_Enumerator.Enumerator.MoveNext()) { _Node.Next = new Node { Value = _Enumerator.Enumerator.Current, Next = null }; _Node = _Node.Next; return true; } return false; } public void Reset() { throw new NotImplementedException(); } } 

这使用reflection创建新实例,然后在新实例上设置值。 我也从深度的C#中发现这一章非常有用。 迭代器块实现细节:自动生成的状态机

 static void Main() { var counter = new CountingClass(); var firstIterator = counter.CountingEnumerator(); Console.WriteLine("First list"); firstIterator.MoveNext(); Console.WriteLine(firstIterator.Current); Console.WriteLine("First list cloned"); var secondIterator = EnumeratorCloner.Clone(firstIterator); Console.WriteLine("Second list"); secondIterator.MoveNext(); Console.WriteLine(secondIterator.Current); secondIterator.MoveNext(); Console.WriteLine(secondIterator.Current); secondIterator.MoveNext(); Console.WriteLine(secondIterator.Current); Console.WriteLine("First list"); firstIterator.MoveNext(); Console.WriteLine(firstIterator.Current); firstIterator.MoveNext(); Console.WriteLine(firstIterator.Current); } public class CountingClass { public IEnumerator CountingEnumerator() { int i = 1; while (true) { yield return i; i++; } } } public static class EnumeratorCloner { public static T Clone(T source) where T : class, IEnumerator { var sourceType = source.GetType().UnderlyingSystemType; var sourceTypeConstructor = sourceType.GetConstructor(new Type[] { typeof(Int32) }); var newInstance = sourceTypeConstructor.Invoke(new object[] { -2 }) as T; var nonPublicFields = source.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance); var publicFields = source.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance); foreach (var field in nonPublicFields) { var value = field.GetValue(source); field.SetValue(newInstance, value); } foreach (var field in publicFields) { var value = field.GetValue(source); field.SetValue(newInstance, value); } return newInstance; } } 

此答案也用于以下问题是否可以克隆IEnumerable实例,保存迭代状态的副本?

为什么不将它作为扩展方法:

 public static IEnumerator Clone(this IEnumerator original) { foreach(var v in original) yield return v; } 

这将基本上创建并返回一个新的枚举器,而不完全评估原始。

编辑:是的,我误读了。 保罗是对的,这只适用于IEnumerable。

这可能有所帮助。 它需要一些代码来调用IEnumerator上的Dispose():

 class Program { static void Main(string[] args) { //var list = MyClass.DequeueAll().ToList(); //var list2 = MyClass.DequeueAll().ToList(); var clonable = MyClass.DequeueAll().ToClonable(); var list = clonable.Clone().ToList(); var list2 = clonable.Clone()ToList(); var list3 = clonable.Clone()ToList(); } } class MyClass { static Queue list = new Queue(); static MyClass() { list.Enqueue("one"); list.Enqueue("two"); list.Enqueue("three"); list.Enqueue("four"); list.Enqueue("five"); } public static IEnumerable DequeueAll() { while (list.Count > 0) yield return list.Dequeue(); } } static class Extensions { public static IClonableEnumerable ToClonable(this IEnumerable e) { return new ClonableEnumerable(e); } } class ClonableEnumerable : IClonableEnumerable { List items = new List(); IEnumerator underlying; public ClonableEnumerable(IEnumerable underlying) { this.underlying = underlying.GetEnumerator(); } public IEnumerator GetEnumerator() { return new ClonableEnumerator(this); } IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } private object GetPosition(int position) { if (HasPosition(position)) return items[position]; throw new IndexOutOfRangeException(); } private bool HasPosition(int position) { lock (this) { while (items.Count <= position) { if (underlying.MoveNext()) { items.Add(underlying.Current); } else { return false; } } } return true; } public IClonableEnumerable Clone() { return this; } class ClonableEnumerator : IEnumerator { ClonableEnumerable enumerable; int position = -1; public ClonableEnumerator(ClonableEnumerable enumerable) { this.enumerable = enumerable; } public T Current { get { if (position < 0) throw new Exception(); return (T)enumerable.GetPosition(position); } } public void Dispose() { } object IEnumerator.Current { get { return this.Current; } } public bool MoveNext() { if(enumerable.HasPosition(position + 1)) { position++; return true; } return false; } public void Reset() { position = -1; } } } interface IClonableEnumerable : IEnumerable { IClonableEnumerable Clone(); } 

“可克隆”枚举器的目的主要是为了能够保存迭代位置并能够在以后返回它。 这意味着,迭代容器必须提供比IEnumerable更丰富的接口。 它实际上是IEnumerableIList之间的东西。 使用IList您可以使用整数索引作为枚举器,或创建一个简单的不可变包装类,保持对列表和当前位置的引用。

如果你的容器不支持随机访问并且只能向前迭代(如单向链表),那么它必须至少提供获取下一个元素的能力,引用前一个元素或某个“迭代状态”你可以保存在你的迭代器中。 所以,界面可能如下所示:

 interface IIterable { IIterator GetIterator(); // returns an iterator positioned at start IIterator GetNext(IIterator prev); // returns an iterator positioned at the next element from the given one } interface IIterator { T Current { get; } IEnumerable AllRest { get; } } 

请注意,迭代器是不可变的 ,它不能“向前移动”,我们只能要求我们的可迭代容器给我们一个指向下一个位置的新迭代器。 这样做的好处是,只要您需要,您可以将迭代器存储在任何位置,例如,有一堆迭代器,并在需要时返回到先前保存的位置。 您可以通过分配变量来保存当前位置以供以后使用,就像使用整数索引一样。

如果需要使用标准语言迭代function(如foraech或LinQ)从给定位置迭代到容器末尾,则AllRest属性非常有用。 它不会改变迭代器的位置(记住,我们的迭代器是不可变的)。 实现可以反复GetNextyleid return

GetNext方法实际上可以是迭代器本身的一部分,如下所示:

 interface IIterable { IIterator GetIterator(); // returns an iterator positioned at start } interface IIterator { T Current { get; } IIterator GetNext { get; } // returns an iterator positioned at the next element from the given one IEnumerable AllRest { get; } } 

这几乎是一样的。 确定下一个状态的逻辑只是从容器实现移到迭代器实现。 请注意,迭代器仍然是不可变的 。 你不能“向前移动”,你只能得到另一个,指向下一个元素。

已经有一种方法可以创建一个新的枚举器 – 就像你创建第一个枚举器一样:IEnumerable.GetEnumerator。 我不确定为什么你需要另一种机制来做同样的事情。

根据DRY原则的精神,我很好奇你为什么要在创建枚举类和枚举器类中重复创建新的IEnumerator实例。 您将强制调查员维持超出要求的额外状态。

例如,想象一下链表的枚举器。 对于IEnumerable的基本实现,该类只需要保持对当前节点的引用。 但是为了支持你的克隆,它还需要保持对列表头部的引用 – 否则它对*没用。 为什么要将这个额外状态添加到枚举器中,何时可以转到源(IEnumerable)并获取另一个枚举器?

为什么要将需要测试的代码路径数量增加一倍? 每当你开辟一种制造物体的新方法时,你就会增加复杂性。

*如果你实现了Reset,你还需要头指针,但是根据文档 ,Reset仅适用于COM互操作,并且你可以自由地抛出NotSupportedException。