【问题标题】:Is there a built-in way to convert IEnumerator to IEnumerable是否有将 IEnumerator 转换为 IEnumerable 的内置方法
【发布时间】:2009-06-22 21:57:37
【问题描述】:

是否有将IEnumerator<T> 转换为IEnumerable<T> 的内置方法?

【问题讨论】:

    标签: c# .net linq


    【解决方案1】:

    我能想到的最简单的转换方法是通过 yield 语句

    public static IEnumerable<T> ToIEnumerable<T>(this IEnumerator<T> enumerator) {
      while ( enumerator.MoveNext() ) {
        yield return enumerator.Current;
      }
    }
    

    与列表版本相比,这具有在返回 IEnumerable 之前不枚举整个列表的优点。使用 yield 语句,您只会遍历您需要的项目,而使用列表版本,您将首先遍历列表中的所有项目,然后是您需要的所有项目。

    为了更有趣,您可以将其更改为

    public static IEnumerable<K> Select<K,T>(this IEnumerator<T> e, 
                                             Func<K,T> selector) {
          while ( e.MoveNext() ) {
            yield return selector(e.Current);
          }
        }
    

    然后您就可以在您的枚举器上使用 linq,例如:

    IEnumerator<T> enumerator;
    var someList = from item in enumerator
                   select new classThatTakesTInConstructor(item);
    

    【讨论】:

    • -1:这不能满足 IEnumerable 应该能够被多次迭代的事实;在这里它只能执行一次,因为源 IEnumerator 将被用完。您需要第二次缓存来自 IEnumerator 的项目。
    • 好吧,您是在说明问题中未说明(至少明确说明)的要求。如果不需要多次迭代,缓存会影响性能,Sam 表示性能是一个非常重要的问题,因此不包括缓存
    • *您需要第二次缓存来自 IEnumerator 的项目。 * - 不。如果这是他们想要的,则由调用者重置枚举器:foreach(var x in fooEnumerator.ToIEnumerable&lt;Foo&gt;()) { ... }; fooEnumerator.Reset(); foreach(var x in fooEnumerator.ToIEnumerable&lt;Foo&gt;()) { ... };
    • @JimBalter 从技术上讲它没有定义任何类。该代码是代码构造的语法糖,它定义了两个类和这两个类,然后再次删除,因为类是编译时构造。所以我猜评论的目的是代码不需要任何额外的类(除了保存方法的类)
    • @JimBalter 规范中绝对没有任何内容需要在上述代码中定义任何类。将上述内容编译为 IL 而不发出任何额外的类并简单地重用现有类是完全合法的
    【解决方案2】:

    您可以使用以下有点工作。

    public class FakeEnumerable<T> : IEnumerable<T> {
      private IEnumerator<T> m_enumerator;
      public FakeEnumerable(IEnumerator<T> e) {
        m_enumerator = e;
      }
      public IEnumerator<T> GetEnumerator() { 
        return m_enumerator;
      }
      // Rest omitted 
    }
    

    当人们期望对 GetEnumerator 的连续调用返回不同的枚举器而不是相同的枚举器时,这会给您带来麻烦。但是,如果它是在非常受限的情况下一次性使用,这可能会解除您的阻止。

    我确实建议您尝试不要这样做,因为我认为最终它会再次困扰您。

    乔纳森建议的方式是一个更安全的选择。您可以扩展枚举器并创建剩余项目的List&lt;T&gt;

    public static List<T> SaveRest<T>(this IEnumerator<T> e) {
      var list = new List<T>();
      while ( e.MoveNext() ) {
        list.Add(e.Current);
      }
      return list;
    }
    

    【讨论】:

    • 只要你只调用一次 GetEnumerator() 就可以工作——大多数调用的东西都希望得到自己的副本。 IE。一旦你完成了整个事情,你就不能重新开始。
    • @Jonathan,是的,完全正确(在答案中注明)。正是我说“有点”的原因
    • 是的,我做了这样的事情......另一个问题是,当你得到一个枚举器时,它的位置为 -1,所以以这种方式包装它会导致跳过
    • @Jared,你的意思是 SaveRest(this IEnumerator enumerable) 不......这是一个危险的函数,作为副作用它枚举并返回一个列表......我最终编码围绕这个看到:stackoverflow.com/questions/1018407/… 但问题是我在那里失去了优雅
    • @Sam,除了 IList,你还会返回什么?我选择 List 而不是迭代器,因为它消除了处理 IEnumerater 一次性的问题。如果您立即创建 List,则没有 dispose 问题。如果选择迭代器路径,则必须处理 IEnumerator 的生命周期
    【解决方案3】:

    EnumeratorEnumerable&lt;T&gt;

    IEnumerator&lt;T&gt;IEnumerable&lt;T&gt; 的线程安全、可重置适配器

    我使用 C++ forward_iterator 概念中的枚举器参数。

    我同意这会导致混淆,因为太多人确实会认为枚举器是 /like/ 可枚举的,但事实并非如此。

    但是,IEnumerator 包含 Reset 方法这一事实加剧了混乱。这是我对最正确实现的想法。它利用 IEnumerator.Reset() 的实现

    Enumerable 和 Enumerator 之间的主要区别在于,Enumerable 可能能够同时创建多个 Enumerator。这个实现做了很多工作来确保EnumeratorEnumerable&lt;T&gt; 类型永远不会发生这种情况。有两个EnumeratorEnumerableModes:

    • Blocking(意味着第二个调用者将简单地等到第一个枚举完成)
    • NonBlocking(意味着对枚举数的第二个(并发)请求只会引发异常)

    注意 1: 74 行是实现,79 行是测试代码 :)

    注意 2:为了方便,我没有参考任何单元测试框架

    using System;
    using System.Diagnostics;
    using System.Linq;
    using System.Collections;
    using System.Collections.Generic;
    using System.Threading;
    
    namespace EnumeratorTests
    {
        public enum EnumeratorEnumerableMode
        {
            NonBlocking,
            Blocking,
        }
    
        public sealed class EnumeratorEnumerable<T> : IEnumerable<T>
        {
            #region LockingEnumWrapper
    
            public sealed class LockingEnumWrapper : IEnumerator<T>
            {
                private static readonly HashSet<IEnumerator<T>> BusyTable = new HashSet<IEnumerator<T>>();
                private readonly IEnumerator<T> _wrap;
    
                internal LockingEnumWrapper(IEnumerator<T> wrap, EnumeratorEnumerableMode allowBlocking) 
                {
                    _wrap = wrap;
    
                    if (allowBlocking == EnumeratorEnumerableMode.Blocking)
                        Monitor.Enter(_wrap);
                    else if (!Monitor.TryEnter(_wrap))
                        throw new InvalidOperationException("Thread conflict accessing busy Enumerator") {Source = "LockingEnumWrapper"};
    
                    lock (BusyTable)
                    {
                        if (BusyTable.Contains(_wrap))
                            throw new LockRecursionException("Self lock (deadlock) conflict accessing busy Enumerator") { Source = "LockingEnumWrapper" };
                        BusyTable.Add(_wrap);
                    }
    
                    // always implicit Reset
                    _wrap.Reset();
                }
    
                #region Implementation of IDisposable and IEnumerator
    
                public void Dispose()
                {
                    lock (BusyTable)
                        BusyTable.Remove(_wrap);
    
                    Monitor.Exit(_wrap);
                }
                public bool MoveNext()      { return _wrap.MoveNext(); }
                public void Reset()         { _wrap.Reset(); }
                public T Current            { get { return _wrap.Current; } }
                object IEnumerator.Current  { get { return Current; } }
    
                #endregion
            }
    
            #endregion
    
            private readonly IEnumerator<T> _enumerator;
            private readonly EnumeratorEnumerableMode _allowBlocking;
    
            public EnumeratorEnumerable(IEnumerator<T> e, EnumeratorEnumerableMode allowBlocking)
            {
                _enumerator = e;
                _allowBlocking = allowBlocking;
            }
    
            private LockRecursionPolicy a;
            public IEnumerator<T> GetEnumerator()
            {
                return new LockingEnumWrapper(_enumerator, _allowBlocking);
            }
    
            IEnumerator IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }
        }
    
        class TestClass
        {
            private static readonly string World = "hello world\n";
    
            public static void Main(string[] args)
            {
                var master = World.GetEnumerator();
                var nonblocking = new EnumeratorEnumerable<char>(master, EnumeratorEnumerableMode.NonBlocking);
                var blocking    = new EnumeratorEnumerable<char>(master, EnumeratorEnumerableMode.Blocking);
    
                foreach (var c in nonblocking)  Console.Write(c); // OK (implicit Reset())
                foreach (var c in blocking)     Console.Write(c); // OK (implicit Reset())
                foreach (var c in nonblocking)  Console.Write(c); // OK (implicit Reset())
                foreach (var c in blocking)     Console.Write(c); // OK (implicit Reset())
    
                try
                {
                    var willRaiseException = from c1 in nonblocking from c2 in nonblocking select new {c1, c2};
                    Console.WriteLine("Cartesian product: {0}", willRaiseException.Count()); // RAISE
                }
                catch (Exception e) { Console.WriteLine(e); }
    
                foreach (var c in nonblocking)  Console.Write(c); // OK (implicit Reset())
                foreach (var c in blocking)     Console.Write(c); // OK (implicit Reset())
    
                try
                {
                    var willSelfLock = from c1 in blocking from c2 in blocking select new { c1, c2 };
                    Console.WriteLine("Cartesian product: {0}", willSelfLock.Count()); // LOCK
                }
                catch (Exception e) { Console.WriteLine(e); }
    
                // should not externally throw (exceptions on other threads reported to console)
                if (ThreadConflictCombinations(blocking, nonblocking))
                    throw new InvalidOperationException("Should have thrown an exception on background thread");
                if (ThreadConflictCombinations(nonblocking, nonblocking))
                    throw new InvalidOperationException("Should have thrown an exception on background thread");
    
                if (ThreadConflictCombinations(nonblocking, blocking))
                    Console.WriteLine("Background thread timed out");
                if (ThreadConflictCombinations(blocking, blocking))
                    Console.WriteLine("Background thread timed out");
    
                Debug.Assert(true); // Must be reached
            }
    
            private static bool ThreadConflictCombinations(IEnumerable<char> main, IEnumerable<char> other)
            {
                try
                {
                    using (main.GetEnumerator())
                    {
                        var bg = new Thread(o =>
                            {
                                try { other.GetEnumerator(); }
                                catch (Exception e) { Report(e); }
                            }) { Name = "background" };
                        bg.Start();
    
                        bool timedOut = !bg.Join(1000); // observe the thread waiting a full second for a lock (or throw the exception for nonblocking)
    
                        if (timedOut)
                            bg.Abort();
    
                        return timedOut;
                    }
                } catch
                {
                    throw new InvalidProgramException("Cannot be reached");
                }
            }
    
            static private readonly object ConsoleSynch = new Object();
            private static void Report(Exception e)
            {
                lock (ConsoleSynch)
                    Console.WriteLine("Thread:{0}\tException:{1}", Thread.CurrentThread.Name, e);
            }
        }
    }
    

    注 3: 我认为线程锁定的实现(尤其是在BusyTable 附近)相当丑陋;但是,我不想求助于ReaderWriterLock(LockRecursionPolicy.NoRecursion),也不想为SpinLock假设.Net 4.0@

    【讨论】:

    • 我喜欢这个主意。我同意这是唯一的“正确”实现。但我认为阻塞模式有点矫枉过正。对于大多数用例来说,使用并发枚举应该是好的。
    • 这完全没有必要……看我的回答。
    【解决方案4】:

    不,IEnumerator 和 IEnumerable 完全是不同的野兽。

    【讨论】:

    • 微软的反直觉命名!
    • 它们可能是不同的东西,但完全不同的野兽
    • 这不是一个有用的答案并且具有误导性。 IEnumerable 的唯一目的是通过 GetEnumerator 方法提供 IEnumerator。所以它们是紧密相关的,而且名字不是“违反直觉的”。 IEnumerable 是可以枚举的东西,因此它必须提供一个枚举器来枚举它。枚举器是枚举某物的东西。人们很难理解这一点,因为他们让眼睛呆滞而不是集中注意力。
    【解决方案5】:

    正如 Jason Watts 所说——不,不是直接的。

    如果你真的想要,你可以遍历 IEnumerator,将项目放入 List,然后返回,但我猜这不是你想要做的。

    你不能去那个方向的基本原因(IEnumerator 到一个 IEnumerable)是 IEnumerable 代表一个可以枚举的集合,但是 IEnumerator 是一个特定的枚举一组项目——你不能把特定的实例变回创建它的东西。

    【讨论】:

    • 我希望在 Skip 实现中做这样的事情,内置的 Linq Skip 是一个完整的 Dog
    • 请参阅:stackoverflow.com/questions/1018407/… ... 我正在寻找一种更简洁的方法来实现 Skip(因此它不在 IEnumerator 上调用),但它涉及 IEnumerable 包装
    【解决方案6】:
    static class Helper
    {
      public static List<T> SaveRest<T>(this IEnumerator<T> enumerator)
      {
        var list = new List<T>();
        while (enumerator.MoveNext())
        {
          list.Add(enumerator.Current);
        }
        return list;
      }
      public static ArrayList SaveRest(this IEnumerator enumerator)
      {
        var list = new ArrayList();
        while (enumerator.MoveNext())
        {
          list.Add(enumerator.Current);
        }
        return list;
      }
    }
    

    【讨论】:

      【解决方案7】:

      这是我写的一个变种...具体有点不同。我想在IEnumerable&lt;T&gt; 上做一个MoveNext(),检查结果,然后将所有内容滚动到一个“完整”的新IEnumerator&lt;T&gt; 中(这样甚至包括我已经提取的IEnumerable&lt;T&gt; 的元素)

      // Simple IEnumerable<T> that "uses" an IEnumerator<T> that has
      // already received a MoveNext(). "eats" the first MoveNext() 
      // received, then continues normally. For shortness, both IEnumerable<T>
      // and IEnumerator<T> are implemented by the same class. Note that if a
      // second call to GetEnumerator() is done, the "real" IEnumerator<T> will
      // be returned, not this proxy implementation.
      public class EnumerableFromStartedEnumerator<T> : IEnumerable<T>, IEnumerator<T>
      {
          public readonly IEnumerator<T> Enumerator;
      
          public readonly IEnumerable<T> Enumerable;
      
          // Received by creator. Return value of MoveNext() done by caller
          protected bool FirstMoveNextSuccessful { get; set; }
      
          // The Enumerator can be "used" only once, then a new enumerator
          // can be requested by Enumerable.GetEnumerator() 
          // (default = false)
          protected bool Used { get; set; }
      
          // The first MoveNext() has been already done (default = false)
          protected bool DoneMoveNext { get; set; }
      
          public EnumerableFromStartedEnumerator(IEnumerator<T> enumerator, bool firstMoveNextSuccessful, IEnumerable<T> enumerable)
          {
              Enumerator = enumerator;
              FirstMoveNextSuccessful = firstMoveNextSuccessful;
              Enumerable = enumerable;
          }
      
          public IEnumerator<T> GetEnumerator()
          {
              if (Used)
              {
                  return Enumerable.GetEnumerator();
              }
      
              Used = true;
              return this;
          }
      
          IEnumerator IEnumerable.GetEnumerator()
          {
              return GetEnumerator();
          }
      
          public T Current
          {
              get
              {
                  // There are various school of though on what should
                  // happens if called before the first MoveNext() or
                  // after a MoveNext() returns false. We follow the 
                  // "return default(TInner)" school of thought for the
                  // before first MoveNext() and the "whatever the 
                  // Enumerator wants" for the after a MoveNext() returns
                  // false
                  if (!DoneMoveNext)
                  {
                      return default(T);
                  }
      
                  return Enumerator.Current;
              }
          }
      
          public void Dispose()
          {
              Enumerator.Dispose();
          }
      
          object IEnumerator.Current
          {
              get
              {
                  return Current;
              }
          }
      
          public bool MoveNext()
          {
              if (!DoneMoveNext)
              {
                  DoneMoveNext = true;
                  return FirstMoveNextSuccessful;
              }
      
              return Enumerator.MoveNext();
          }
      
          public void Reset()
          {
              // This will 99% throw :-) Not our problem.
              Enumerator.Reset();
      
              // So it is improbable we will arrive here
              DoneMoveNext = true;
          }
      }
      

      用途:

      var enumerable = someCollection<T>;
      
      var enumerator = enumerable.GetEnumerator();
      bool res = enumerator.MoveNext();
      // do whatever you want with res/enumerator.Current
      
      var enumerable2 = new EnumerableFromStartedEnumerator<T>(enumerator, res, enumerable);
      

      现在,将通过枚举器enumerator 给出将被请求到enumerable2 的第一个GetEnumerator()。从第二个开始,enumerable.GetEnumerator() 将被使用。

      【讨论】:

        【解决方案8】:

        使用 Factory 以及修复 JaredPar's answer 中缓存的 IEnumerator 问题的解决方案允许更改枚举方式。

        考虑一个简单的例子:我们想要自定义List&lt;T&gt; 包装器,它允许以相反的顺序枚举以及默认枚举。 List&lt;T&gt; 已经实现了IEnumerator 用于默认枚举,我们只需要创建以相反顺序枚举的IEnumerator。 (我们不会使用List&lt;T&gt;.AsEnumerable().Reverse(),因为它会枚举列表两次)

        public enum EnumerationType {
            Default = 0,
            Reverse
        }
        
        public class CustomList<T> : IEnumerable<T> {
            private readonly List<T> list;
        
            public CustomList(IEnumerable<T> list) => this.list = new List<T>(list);
            
            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
            
            //Default IEnumerable method will return default enumerator factory
            public IEnumerator<T> GetEnumerator() 
                => GetEnumerable(EnumerationType.Default).GetEnumerator();
        
            public IEnumerable<T> GetEnumerable(EnumerationType enumerationType)
                => enumerationType switch {
                    EnumerationType.Default => new DefaultEnumeratorFactory(list),
                    EnumerationType.Reverse => new ReverseEnumeratorFactory(list)
                };
            
            //Simple implementation of reverse list enumerator
            private class ReverseEnumerator : IEnumerator<T> {
                private readonly List<T> list;
                private int index;
        
                internal ReverseEnumerator(List<T> list) {
                    this.list = list;
                    index = list.Count-1;
                    Current = default;
                }
        
                public void Dispose() { }
        
                public bool MoveNext() {
                    if(index >= 0) {
                        Current = list[index];
                        index--;
                        return true;
                    }
                    Current = default;
                    return false;
                }
        
                public T Current { get; private set; }
        
                object IEnumerator.Current => Current;
        
                void IEnumerator.Reset() {
                    index = list.Count - 1;
                    Current = default;
                }
            }
            
            private abstract class EnumeratorFactory : IEnumerable<T> {
                protected readonly List<T> List;
                protected EnumeratorFactory(List<T> list) => List = list;
                
                IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
                
                public abstract IEnumerator<T> GetEnumerator();
            }
        
        
            private class DefaultEnumeratorFactory : EnumeratorFactory {
                
                public DefaultEnumeratorFactory(List<T> list) : base(list) { }
                
                //Default enumerator is already implemented in List<T>
                public override IEnumerator<T> GetEnumerator() => List.GetEnumerator();
            }
        
        
            private class ReverseEnumeratorFactory : EnumeratorFactory {
        
                public ReverseEnumeratorFactory(List<T> list) : base(list) { }
        
                public override IEnumerator<T> GetEnumerator() => new ReverseEnumerator(List);
            }
            
        }
        

        【讨论】:

          【解决方案9】:

          这里的其他答案……很奇怪。 IEnumerable&lt;T&gt; 只有一种方法,GetEnumerator()。而IEnumerable&lt;T&gt; 必须实现IEnumerable,它也只有一个方法GetEnumerator()(区别在于T 上一个是通用的,而另一个不是)。所以应该清楚如何将IEnumerator&lt;T&gt; 变成IEnumerable&lt;T&gt;

              // using modern expression-body syntax
              public class IEnumeratorToIEnumerable<T> : IEnumerable<T>
              {
                  private readonly IEnumerator<T> Enumerator;
          
                  public IEnumeratorToIEnumerable(IEnumerator<T> enumerator) =>
                      Enumerator = enumerator;
          
                  public IEnumerator<T> GetEnumerator() => Enumerator;
                  IEnumerator IEnumerable.GetEnumerator() => Enumerator;
              }
          
              foreach (var foo in new IEnumeratorToIEnumerable<Foo>(fooEnumerator))
                  DoSomethingWith(foo);
          
              // and you can also do:
          
              var fooEnumerable = new IEnumeratorToIEnumerable<Foo>(fooEnumerator);
          
              foreach (var foo in fooEnumerable)
                  DoSomethingWith(foo);
          
              // Some IEnumerators automatically repeat after MoveNext() returns false,
              // in which case this is a no-op, but generally it's required.
              fooEnumerator.Reset();
          
              foreach (var foo in fooEnumerable)
                  DoSomethingElseWith(foo);
          

          但是,这些都不需要,因为 IEnumerator&lt;T&gt; 不附带 IEnumerable&lt;T&gt; 从其 GetEnumerator 方法返回它的实例是不寻常的。如果您正在编写自己的IEnumerator&lt;T&gt;,您当然应该提供IEnumerable&lt;T&gt;。实际上恰恰相反...... IEnumerator&lt;T&gt; 旨在成为一个私有类,它迭代实现 IEnumerable&lt;T&gt; 的公共类的实例。

          【讨论】:

          • @sehe 我最初并没有认识到接受的答案是一样的,因为它是用“假”和许多其他废话来表达的,比如将枚举数的结果复制到列表中而不是简单地使用重置。而且我一点也不“对这种方法充满热情”......正如我的最后一段所表明的那样,采用任意 IEnumerator 并将其包装在 IEnumerable 中通常是错误的......几乎总是有一个提供自己的 IEnumerable (私有)IEnumerator。
          • 问题是,似乎很少有人了解这两个接口之间的关系......即使是在 2018 年,更不用说回到 2009 年了。所以我希望这会有所帮助。
          • 甚至连yield语句都看不到 -- yield return当然很方便,但它模糊了人们的理解。当您编写使用yield returnIEnumerable 方法时,编译器实际上会为您生成两个 匿名类……一个IEnumerator 和一个返回前者实例的IEnumerable。然后,您的方法只返回 IEnumerable 类的实例。因此,在他们的答案中编写这些yield return 循环的人似乎不知道他们增加了多少开销。
          • @JimBalter 有时开销可能具有欺骗性。如果我运行您的解决方案,而我的解决方案只是从 1 数到 100000。您的解决方案使用了我使用的 80% 的时间。另一方面,如果我在这里运行接受的答案stackoverflow.com/questions/13001578/…,那么在某些时候我的解决方案比您的解决方案运行得更快。交叉点在我机器上输入值 2000 的区域内
          • 我的热情是基于样式缺失的部分(例如,如果你想指出这些接口之间的关系,你巧妙地避免了惯用的显式接口实现)。
          猜你喜欢
          • 1970-01-01
          • 2012-02-14
          • 2017-07-21
          • 2010-11-19
          • 1970-01-01
          • 2012-12-30
          • 2022-07-25
          • 2010-10-04
          • 1970-01-01
          相关资源
          最近更新 更多