代码之家  ›  专栏  ›  技术社区  ›  Paul Hollingsworth

为什么IEnumerator不能被克隆?

  •  8
  • Paul Hollingsworth  · 技术社区  · 17 年前

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

    我想要的是:

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

    缺少Clone方法意味着对序列进行枚举的任何函数/递归习惯用法都不起作用。

    这也意味着我不能“无缝地”使IEnumerable的行为类似于Lisp“列表”(使用car/cdr递归枚举)。i、 e.cdr的唯一实施 有些是数不清的 )“将是非常低效的。

    有人能推荐一个现实的、有用的IEnumerable对象示例,它不能提供有效的“Clone()”方法吗?“产量”结构会有问题吗?

    有人能提出解决办法吗?

    8 回复  |  直到 17 年前
        1
  •  23
  •   Daniel Earwicker    17 年前

    IEnumerable Clone ,你需要 克隆 ,所以你不应该使用

    或者更准确地说,您不应该将其用作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示例 “产量”结构?

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

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

    这有两个问题:第一,一个 克隆 函数的编写不会特别简单(以及 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 操纵

    我想知道,最低级别是多少 “原始”我需要这样的 我想用一个 我的方案中的IEnumerable解释器 而不是作为一个内置的。

    我想最简单的答案是看看里面 Abelson and Sussman 尤其是 the part about streams . 数不清

        2
  •  4
  •   Reed Copsey    17 年前

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

    但是,您将失去枚举器的流式处理功能—因为您是新的“克隆”,这将导致第一个枚举器完全计算。

        3
  •  3
  •   Lasse V. Karlsen    16 年前

    如果可以放弃原始枚举数,即不再使用它,则可以实现一个“克隆”函数,将原始枚举数作为一个或多个枚举数的源。

    换句话说,您可以构建如下内容:

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

    它们可以在内部共享原始枚举数和链接列表,以跟踪枚举值。

    • Lazy枚举是两个枚举数中需要一个尚未从原始枚举数检索到的值的第一个枚举数,它将获取该值并在生成该值之前将其存储到链接列表中

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

    这是源代码。如果使用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();
        }
    }
    
        4
  •  1
  •   Community Mohan Dere    9 年前

    这将使用反射创建新实例,然后在新实例上设置值。我还发现C#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?

        5
  •  1
  •   C-F    10 年前

    “clonable”枚举数的目的主要是保存迭代位置,并在以后返回到该位置。这意味着,迭代容器必须提供比 IEnumerable 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; }
    }
    

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

    这个 AllRest 属性在需要使用标准语言迭代功能从给定位置迭代到容器末尾时非常有用,如 foraech 或者林克。它不会改变迭代器的位置(记住,我们的迭代器是不可变的)。可以重复执行 GetNext yleid return .

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

    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
  •  0
  •   Aaron Aaron    17 年前

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

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

    这将基本上创建并返回一个新的枚举数,而无需完全计算原始枚举数。

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

        7
  •  0
  •   tofi9    17 年前

    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
  •  -2
  •   Joe White    17 年前

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

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

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

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

    * 如果实现了重置,则还需要头指针,但是 according to the docs

    推荐文章