【问题标题】:Why can't IEnumerator's be cloned?为什么不能克隆 IEnumerator?
【发布时间】:2010-10-22 22:06:02
【问题描述】:

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

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

我想要什么:

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

我想不出一个 IEnumerable 的实现,它不能提供一个有效的可克隆 IEnumerator(向量、链表等),都可以提供一个简单的 IEnumerator 的 Clone() 实现,如上所述。 . 无论如何,这比提供 Reset() 方法更容易!)。

没有 Clone 方法意味着枚举序列的任何函数/递归习惯用法都不起作用。

这也意味着我不能“无缝”使 IEnumerable 的行为类似于 Lisp“列表”(您使用 car/cdr 递归枚举)。即“(cdr some IEnumerable)”的唯一实现将非常低效。

谁能提出一个现实的、有用的IEnumerable 对象示例,它不能提供有效的“Clone()”方法? “yield”结构会不会有问题?

任何人都可以提出解决方法吗?

【问题讨论】:

  • 我不知道你为什么想要一个可克隆的 IEnumerator,但可克隆的 IEnumerable 有帮助吗?
  • 仅供参考,它可以被克隆。我很久以前的回答显示了如何。它实际上是克隆 IEnumerator 而不是从它创建的值。

标签: c# .net ienumerable


【解决方案1】:

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

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

public class Link<TValue>
{
    private readonly TValue value;
    private readonly Link<TValue> next;

    public Link(TValue value, Link<TValue> next)
    {
        this.value = value;
        this.next = next;
    } 

    public TValue Value 
    { 
        get { return value; }
    }

    public Link<TValue> Next 
    {
        get { return next; }
    }

    public IEnumerable<TValue> ToEnumerable()
    {
        for (Link<TValue> v = this; v != null; v = v.next)
            yield return v.value;
    }
}

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

回答你的问题:

任何人都可以提出一个现实的, 有用的 IEnumerable 示例 无法做到的对象 提供有效的“Clone()”方法? 会不会有问题 “收益”结构?

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

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

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

另一个例子:

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

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

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

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

IEnumerable<byte> 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 实现?

一旦创建了克隆,两个实例现在都必须从它们共享的缓冲区系统中工作。这是可能的,但实际上它不是必需的——这不是这些序列的设计使用方式。您将它们纯粹“功能性地”对待,就像值一样,递归地对它们应用过滤器,而不是“强制性地”记住序列中的位置。它比低级的car/cdr 操作要干净一些。

另一个问题:

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

我认为简短的答案是查看Abelson and Sussman,尤其是the part about streamsIEnumerable 是一个流,而不是一个列表。它们描述了您如何需要特殊版本的地图、过滤器、累积等来使用它们。他们还在第 4.2 节中提出了统一列表和流的想法。

【讨论】:

  • 是的,我知道如何写一个链表。我试图避免使所有 C# 代码(特别是任何自定义函数)都必须预先将所有 IEnumerables 转换为链表。我意识到这是不可能的......但现在我想知道为什么他们不让 IEnumerator 可克隆?
  • 查看“为什么”的更新答案,这也是“制作副本”解决方法不太正确的原因。
  • 好吧...我想如果他们也不愿意把 Clone() 放在上面的话,他们不应该把 Reset() 放在上面。
  • Reset 确实是一个错误 - IEnumerable 是可以做到的最好的,但它是一种妥协,因为它继承自具有 Reset 的“版本一”非泛型 IEnumerable。跨度>
  • 嗯,关于“car/cdr”级别太低的有趣点......所以,我想知道,我需要的最低级别的“原始”是什么,这样我可能想做的任何事情在我的 Scheme 解释器中使用 IEnumerable 可以在 scheme 中实现,而不是作为内置 ...
【解决方案2】:

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

但是,您会失去枚举器的流式传输功能 - 因为您是新的“克隆”,所以会导致第一个枚举器完全评估。

【讨论】:

  • 迄今为止最好、最有用的答案......但仍然想举例说明为什么他们不能让 IEnumerator 可克隆......(在我的问题中以粗体突出显示)。
  • 正如我所提到的 - 只要您克隆了枚举器,您利用 IEnumerator 的流式传输(延迟执行)的任何情况都会破坏或至少失去优势。提供克隆功能更多的是成本与收益的问题。
  • 是的,不,我意识到您的解决方案的成本。关键是,如果 IEnumerable/IEnumerator 无法提供有效的克隆,是否有任何现实的实施方法...?
  • 无限(或无限重复)序列将破坏此解决方法。从潜在的无限输入流中读取的序列也是如此。
【解决方案3】:

如果您可以让原始枚举器离开,即。不再使用它,您可以实现一个“克隆”函数,该函数采用原始枚举器,并将其用作一个或多个枚举器的源。

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

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

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

这将允许:

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

这里的问题当然是,如果其中一个枚举器远远领先于另一个,它仍然需要大量内存。

这里是源代码。如果您使用 Subversion,您可以下载带有类库的 Visual Studio 2008 解决方案文件,其中包含以下代码,以及单独的单元测试项目。

存储库:http://vkarlsen.serveftp.com:81/svnStackOverflow/SO847655
用户名和密码都是“guest”,不带引号。

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

public static class EnumeratorExtensions
{
    /// <summary>
    /// "Clones" the specified <see cref="IEnumerator{T}"/> by wrapping it inside N new
    /// <see cref="IEnumerator{T}"/> instances, each can be advanced separately.
    /// See remarks for more information.
    /// </summary>
    /// <typeparam name="T">
    /// The type of elements the <paramref name="enumerator"/> produces.
    /// </typeparam>
    /// <param name="enumerator">
    /// The <see cref="IEnumerator{T}"/> to "clone".
    /// </param>
    /// <param name="clones">
    /// The number of "clones" to produce.
    /// </param>
    /// <returns>
    /// An array of "cloned" <see cref="IEnumerator[T}"/> instances.
    /// </returns>
    /// <remarks>
    /// <para>The cloning process works by producing N new <see cref="IEnumerator{T}"/> instances.</para>
    /// <para>Each <see cref="IEnumerator{T}"/> instance can be advanced separately, over the same
    /// items.</para>
    /// <para>The original <paramref name="enumerator"/> will be lazily evaluated on demand.</para>
    /// <para>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.</para>
    /// </remarks>
    /// <exception cref="ArgumentNullException">
    /// <para><paramref name="enumerator"/> is <c>null</c>.</para>
    /// </exception>
    /// <exception cref="ArgumentOutOfRangeException">
    /// <para><paramref name="clones"/> is less than 2.</para>
    /// </exception>
    public static IEnumerator<T>[] Clone<T>(this IEnumerator<T> enumerator, Int32 clones)
    {
        #region Parameter Validation

        if (Object.ReferenceEquals(null, enumerator))
            throw new ArgumentNullException("enumerator");
        if (clones < 2)
            throw new ArgumentOutOfRangeException("clones");

        #endregion

        ClonedEnumerator<T>.EnumeratorWrapper wrapper = new ClonedEnumerator<T>.EnumeratorWrapper
        {
            Enumerator = enumerator,
            Clones = clones
        };
        ClonedEnumerator<T>.Node node = new ClonedEnumerator<T>.Node
        {
            Value = enumerator.Current,
            Next = null
        };

        IEnumerator<T>[] result = new IEnumerator<T>[clones];
        for (Int32 index = 0; index < clones; index++)
            result[index] = new ClonedEnumerator<T>(wrapper, node);
        return result;
    }
}

internal class ClonedEnumerator<T> : IEnumerator<T>, IDisposable
{
    public class EnumeratorWrapper
    {
        public Int32 Clones { get; set; }
        public IEnumerator<T> 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();
    }
}

【讨论】:

  • 聪明......所以它有点像 Reed Copsey 的答案的懒惰评估版本(所以它只使用列表中的所有元素,如果你真的这样使用它)。
  • 好吧,我稍后会发布一个示例实现,现在正在教一堂课。
  • 很想看到这个 - 前几天我开始为我的答案写同样的东西,但从未完成。
  • 虽然看taoufik的回答。
  • 贴出源代码。例如,可以使用一些优化技术,而不是每个节点只有一个值的链表,每个节点可以包含一个项目数组。这会在某种程度上使设计复杂化,但仍然可行。
【解决方案4】:

这使用反射来创建一个新实例,然后在新实例上设置值。我还发现 C# in Depth 的这一章非常有用。 Iterator block implementation details: auto-generated state machines

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<int> CountingEnumerator()
    {
        int i = 1;
        while (true)
        {
            yield return i;
            i++;
        }
    }
}

public static class EnumeratorCloner
{
    public static T Clone<T>(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;
    }
}

这个答案也被用于以下问题Is it possible to clone an IEnumerable instance, saving a copy of the iteration state?

【讨论】:

  • 这不适用于所有情况,并且依赖于未记录的行为(现在可能有效,但以后可能不会)。请参阅此评论:stackoverflow.com/questions/807991/… 但是,如果您自己创建了枚举器,那么您不仅可以验证其是否可以安全地进行成员克隆,而且您还可以创建一个正确的克隆方法。
【解决方案5】:

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

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

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

interface IIterator<T>
{
    T Current { get; }
    IEnumerable<T> AllRest { get; }
}

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

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

GetNext 方法实际上可以是迭代器本身的一部分,就像这样:

interface IIterable<T>
{
    IIterator<T> GetIterator(); // returns an iterator positioned at start
}

interface IIterator<T>
{
    T Current { get; }
    IIterator<T> GetNext { get; } // returns an iterator positioned at the next element from the given one
    IEnumerable<T> AllRest { get; }
}

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

【讨论】:

    【解决方案6】:

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

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

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

    编辑:是的,我看错了。 Paul 是正确的,这仅适用于 IEnumerable。

    【讨论】:

    • 那行不通——你不能在 IEnumerator 上进行 foreach ,对吧?仅通过 IEnumerable...
    • 这段代码甚至无法编译,你不能 foreach IEnumerator original 并且你不能使用 yield 来返回 IEnumerator。在这两种情况下,您可能指的是 IEnumerable
    【解决方案7】:

    这可能会有所帮助。它需要一些代码来调用 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<string> list = new Queue<string>();
    
        static MyClass()
        {
            list.Enqueue("one");
            list.Enqueue("two");
            list.Enqueue("three");
            list.Enqueue("four");
            list.Enqueue("five");
        }
    
        public static IEnumerable<string> DequeueAll()
        {
            while (list.Count > 0)
                yield return list.Dequeue();
        }
    }
    
    static class Extensions
    {
        public static IClonableEnumerable<T> ToClonable<T>(this IEnumerable<T> e)
        {
            return new ClonableEnumerable<T>(e);
        }
    }
    
    class ClonableEnumerable<T> : IClonableEnumerable<T>
    {
        List<T> items = new List<T>();
        IEnumerator<T> underlying;
    
        public ClonableEnumerable(IEnumerable<T> underlying)
        {
            this.underlying = underlying.GetEnumerator();
        }
    
        public IEnumerator<T> GetEnumerator()
        {
            return new ClonableEnumerator<T>(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<T> Clone()
        {
            return this;
        }
    
    
        class ClonableEnumerator<T> : IEnumerator<T>
        {
            ClonableEnumerable<T> enumerable;
            int position = -1;
    
            public ClonableEnumerator(ClonableEnumerable<T> 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<T> : IEnumerable<T>
    {
        IClonableEnumerable<T> Clone();
    }
    

    【讨论】:

      【解决方案8】:

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

      本着DRY principle 的精神,我很好奇为什么您希望在您的可枚举类和您的枚举类中复制创建新的 IEnumerator 实例的责任。您将强制枚举器保持超出所需的其他状态。

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

      为什么要将需要测试的代码路径数量增加一倍?每次你用一种新的方法来制造一个物体,你都在增加复杂性。

      * 如果您实现了 Reset,您还需要头指针,但 according to the docs,Reset 仅用于 COM 互操作,您可以随意抛出 NotSupportedException。

      【讨论】:

      • 克隆的枚举器不会指向序列中的第一项。它将指向原始枚举器的当前项。目的是能够记住当前位置。
      • 一些枚举器可以克隆他们当前的位置,但很多不能。例如,只进数据库游标;或从网络流或管道读取的任何内容。即使是从本地资源中读取的内容(如文件流)也需要大量工作才能使其可克隆,因为文件句柄只有一个当前位置。
      猜你喜欢
      • 2012-07-05
      • 2011-10-17
      • 1970-01-01
      • 1970-01-01
      • 2015-05-29
      • 1970-01-01
      • 1970-01-01
      • 2014-08-22
      相关资源
      最近更新 更多