【问题标题】:Handling warning for possible multiple enumeration of IEnumerable处理 IEnumerable 可能的多重枚举的警告
【发布时间】:2012-01-04 15:58:32
【问题描述】:

在我的代码中,我需要多次使用 IEnumerable<>,导致 ReSharper 错误“IEnumerable 的可能多重枚举”。

示例代码:

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null || !objects.Any())
        throw new ArgumentException();
        
    var firstObject = objects.First();
    var list = DoSomeThing(firstObject);        
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);
    
    return list;
}
  • 我可以将objects 参数更改为List,然后避免可能的多重枚举,但是我没有得到我可以处理的最高对象。
  • 我可以做的另一件事是将方法开头的IEnumerable 转换为List

 public List<object> Foo(IEnumerable<object> objects)
 {
    var objectList = objects.ToList();
    // ...
 }

但这只是尴尬

在这种情况下你会怎么做?

【问题讨论】:

    标签: c# .net performance resharper


    【解决方案1】:

    IEnumerable 作为参数的问题在于它告诉调用者“我希望枚举这个”。它没有告诉他们您希望枚举多少次。

    我可以将 objects 参数更改为 List,然后避免可能的多重枚举,但是我没有得到 我可以处理的最高对象

    取最高物的目标是崇高的,但它为太多的假设留下了空间。您是否真的希望有人将 LINQ to SQL 查询传递给此方法,只是让您枚举它两次(每次都可能得到不同的结果?)

    这里缺少的语义是调用者可能不花时间阅读方法的细节,可能会假设您只迭代一次 - 所以他们传递给您一个昂贵的对象。您的方法签名并不表示任何一种方式。

    通过将方法签名更改为IList/ICollection,您至少可以让调用者更清楚您的期望是什么,并且他们可以避免代价高昂的错误。

    否则,大多数研究该方法的开发人员可能会认为您只迭代一次。如果采取IEnumerable 如此重要,您应该考虑在方法开始时执行.ToList()

    很遗憾 .NET 没有 IEnumerable + Count + Indexer 的接口,没有添加/删除等方法,我怀疑这是可以解决这个问题的方法。

    【讨论】:

    • ReadOnlyCollection 是否满足您所需的接口要求? msdn.microsoft.com/en-us/library/ms132474.aspx
    • @DanNeely 我建议将IReadOnlyCollection(T)(.net 4.5 的新功能)作为最好的接口来传达IEnumerable(T) 旨在被多次枚举的想法。正如这个答案所述,IEnumerable(T) 本身是如此通用,以至于它甚至可以引用不可重置的内容,这些内容无法在没有副作用的情况下再次枚举。但是IReadOnlyCollection(T) 意味着可重复性。
    • 如果你在方法的开头做.ToList(),你首先要检查参数是否不为空。这意味着您将在两个地方抛出 ArgumentException:如果参数为 null,并且它没有任何项目。
    • 我在IReadOnlyCollection&lt;T&gt;IEnumerable&lt;T&gt; 的语义上阅读了this article,然后发布了a question 关于使用IReadOnlyCollection&lt;T&gt; 作为参数的信息,以及在哪些情况下。我认为我最终找到的conclusion 是要遵循的逻辑。它应该在需要枚举的地方使用,不一定在可能有多个枚举的地方使用。
    • 为什么在 Resharper 中采用 IList 而不是 IEnumerable 会消除此分析警告?通常的做法是采用最开放/通用的类型;所以我看不到IList 如何解决潜在的多次迭代问题。整个警告让我感到非常困惑。
    【解决方案2】:

    如果您的数据总是可重复的,也许不用担心。但是,您也可以展开它 - 如果传入的数据可能很大(例如,从磁盘/网络读取),这尤其有用:

    if(objects == null) throw new ArgumentException();
    using(var iter = objects.GetEnumerator()) {
        if(!iter.MoveNext()) throw new ArgumentException();
    
        var firstObject = iter.Current;
        var list = DoSomeThing(firstObject);  
    
        while(iter.MoveNext()) {
            list.Add(DoSomeThingElse(iter.Current));
        }
        return list;
    }
    

    注意我稍微改变了 DoSomethingElse 的语义,但这主要是为了展示展开的用法。例如,您可以重新包装迭代器。你也可以把它变成一个迭代器块,这很好;那么就没有list - 你会在获得项目时yield return,而不是添加到要返回的列表中。

    【讨论】:

    • +1 这是一个非常好的代码,但是 - 我失去了代码的美观性和可读性。
    • @gdoron 并非一切都很漂亮;p 我不认为上面的内容是不可读的。特别是如果它被制成一个迭代器块 - 非常可爱,IMO。
    • @gdoron 在您明确表示您并不想这样做的问题中;p 此外,如果数据非常大(可能是无限序列),则 ToList() 方法可能不适合不需要是有限的,但仍然可以迭代)。最终,我只是提出一个选择。只有您知道完整的上下文以查看是否有必要。如果你的数据是可重复的,另一个有效的选择是:不要改变任何东西!而是忽略 R#...这一切都取决于上下文。
    • 我认为 Marc 的解决方案相当不错。 1.很清楚‘list’是怎么初始化的,很清楚list是怎么迭代的,对list的哪些成员进行操作的很清楚。
    • @anatol IEnumerable&lt;T&gt; 仅表示“一个序列”;如果该序列来自某个内存列表/数组/等,那么可以肯定:它可能是可重复的——但是——它可能是从套接字、PRNG 或大量其他地方提取数据;从根本上说,IEnumerable&lt;T&gt; 不能保证是可重复的
    【解决方案3】:

    首先,该警告并不总是那么重要。我通常在确保它不是性能瓶颈后禁用它。这只是意味着IEnumerable 被评估了两次,这通常不是问题,除非evaluation 本身需要很长时间。即使确实需要很长时间,在这种情况下,您第一次只使用一个元素。

    在这种情况下,您还可以进一步利用强大的 linq 扩展方法。

    var firstObject = objects.First();
    return DoSomeThing(firstObject).Concat(DoSomeThingElse(objects).ToList();
    

    在这种情况下,可能只评估一次IEnumerable 会有些麻烦,但请先进行分析,看看它是否真的有问题。

    【讨论】:

    • 我经常使用 IQueryable,关闭这些警告非常危险。
    • 在我的书中评估两次是一件坏事。如果该方法采用 IEnumerable,我可能很想将 LINQ to SQL 查询传递给它,这会导致多个 DB 调用。由于方法签名没有表明它可以枚举两次,它应该要么更改签名(接受 IList/ICollection)要么只迭代一次
    • 过早优化并破坏可读性,因此在我的书中可能会进行产生差异的优化。
    • 当你有一个数据库查询时,确保它只被评估一次已经会有所作为。这不仅仅是理论上的未来收益,而是现在可衡量的实际收益。不仅如此,只评估一次不仅对性能有益,而且对正确性也是必需的。执行两次数据库查询可能会产生不同的结果,因为有人可能在两次查询评估之间发出了更新命令。
    • @hvd 我不建议这样做。当然,您不应该枚举IEnumerables,它每次迭代都会给出不同的结果,或者迭代需要很长时间。请参阅 Marc Gravells 的回答 - 他还首先声明“也许别担心”。问题是 - 很多时候你看到这个你没什么好担心的。
    【解决方案4】:

    如果目标确实是为了防止多次枚举,而不是 Marc Gravell 的答案是要阅读的答案,但保持相同的语义,您可以简单地删除多余的 AnyFirst 调用并继续:

    public List<object> Foo(IEnumerable<object> objects)
    {
        if (objects == null)
            throw new ArgumentNullException("objects");
    
        var first = objects.FirstOrDefault();
    
        if (first == null)
            throw new ArgumentException(
                "Empty enumerable not supported.", 
                "objects");
    
        var list = DoSomeThing(first);  
    
        var secondList = DoSomeThingElse(objects);
    
        list.AddRange(secondList);
    
        return list;
    }
    

    请注意,这假定您 IEnumerable 不是通用的,或者至少被限制为引用类型。

    【讨论】:

    • objects 仍在传递给返回列表的 DoSomethingElse,因此您仍然枚举两次的可能性仍然存在。无论哪种方式,如果该集合是一个 LINQ to SQL 查询,您都会访问 DB 两次。
    • @PaulStovell,阅读此内容:“如果目标确实是为了防止多次枚举,那么 Marc Gravell 的答案就是要阅读的答案”=)
    • 在其他一些关于为空列表引发异常的stackoverflow帖子中已提出建议,我将在这里提出建议:为什么在空列表上引发异常?得到一个空列表,给一个空列表。作为一个范例,这非常简单。如果调用者不知道他们传入的是一个空列表,那就有问题了。
    • @Keith Hoffman,示例代码保持与 OP 发布的代码相同的行为,其中使用 First 将为空列表引发异常。我认为不可能说永远不要在空列表上抛出异常或总是在空列表上抛出异常。这取决于...
    • 公平的 Joao。比批评你的帖子更值得深思。
    【解决方案5】:

    在这种情况下,我通常使用 IEnumerable 和 IList 重载我的方法。

    public static IEnumerable<T> Method<T>( this IList<T> source ){... }
    
    public static IEnumerable<T> Method<T>( this IEnumerable<T> source )
    {
        /*input checks on source parameter here*/
        return Method( source.ToList() );
    }
    

    我注意在摘要中解释调用 IEnumerable 将执行 .ToList() 的方法。

    如果要连接多个操作,程序员可以选择更高级别的 .ToList(),然后调用 IList 重载或让我的 IEnumerable 重载来处理。

    【讨论】:

    • 这太可怕了。
    • @MikeChamberlain 是的,它真的很可怕,同时又非常干净。不幸的是,一些繁重的场景需要这种方法。
    • 但是,每次将数组传递给 IEnumerable 参数化方法时,您都会重新创建数组。在这方面我同意@MikeChamberlain,这是一个糟糕的建议。
    • @Krythic 如果您不需要重新创建列表,您可以在其他地方执行 ToList 并使用 IList 重载。这就是这个解决方案的重点。
    【解决方案6】:

    在方法签名中使用IReadOnlyCollection&lt;T&gt;IReadOnlyList&lt;T&gt; 代替IEnumerable&lt;T&gt; 的优点是明确表示您可能需要在迭代之前检查计数,或者出于某些其他原因进行多次迭代。

    但是,它们有一个巨大的缺点,如果您尝试重构代码以使用接口,例如使其更可测试且对动态代理更友好,则会导致问题。关键是 IList&lt;T&gt; 不继承自 IReadOnlyList&lt;T&gt;,对于其他集合及其各自的只读接口也是如此。 (简而言之,这是因为 .NET 4.5 想要保持 ABI 与早期版本的兼容性。But they didn't even take the opportunity to change that in .NET core.

    这意味着,如果您从程序的某个部分获得IList&lt;T&gt;,并希望将其传递给期望IReadOnlyList&lt;T&gt; 的另一部分,则不能! 您可以将IList&lt;T&gt; 作为IEnumerable&lt;T&gt; 传递

    最后,IEnumerable&lt;T&gt; 是所有 .NET 集合(包括所有集合接口)都支持的唯一只读接口。当您意识到您将自己锁定在某些架构选择之外时,任何其他选择都会回来咬您。所以我认为在函数签名中使用它来表达你只想要一个只读集合是正确的类型。

    (请注意,如果底层类型支持这两个接口,您始终可以编写一个简单转换的 IReadOnlyList&lt;T&gt; ToReadOnly&lt;T&gt;(this IList&lt;T&gt; list) 扩展方法,但在重构时您必须在任何地方手动添加它,因为 IEnumerable&lt;T&gt; 始终兼容。)

    与往常一样,这不是绝对的,如果您正在编写大量数据库的代码,而意外的多次枚举将是一场灾难,您可能更喜欢不同的权衡。

    【讨论】:

    • +1 发表一篇旧文章并添加有关使用 .NET 平台提供的新功能(即 IReadOnlyCollection)解决此问题的新方法的文档
    【解决方案7】:

    如果您只需要检查第一个元素,则无需遍历整个集合即可查看它:

    public List<object> Foo(IEnumerable<object> objects)
    {
        object firstObject;
        if (objects == null || !TryPeek(ref objects, out firstObject))
            throw new ArgumentException();
    
        var list = DoSomeThing(firstObject);
        var secondList = DoSomeThingElse(objects);
        list.AddRange(secondList);
    
        return list;
    }
    
    public static bool TryPeek<T>(ref IEnumerable<T> source, out T first)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
    
        IEnumerator<T> enumerator = source.GetEnumerator();
        if (!enumerator.MoveNext())
        {
            first = default(T);
            source = Enumerable.Empty<T>();
            return false;
        }
    
        first = enumerator.Current;
        T firstElement = first;
        source = Iterate();
        return true;
    
        IEnumerable<T> Iterate()
        {
            yield return firstElement;
            using (enumerator)
            {
                while (enumerator.MoveNext())
                {
                    yield return enumerator.Current;
                }
            }
        }
    }
    

    【讨论】:

      【解决方案8】:

      如果他们可能需要多次枚举事物,我喜欢采用 IReadOnlyCollection 的方法。我想提供一个实用方法,可以在将 IEnumerable 转换为 IReadOnlyCollection 时调用该方法。与 ToArray() 或 ToList() 不同,它可以避免额外分配改变底层集合类型的成本:

          public static class EnumerableUtils
          {
              /* Ensures an IEnumerable is only enumerated once  */
              public static IReadOnlyCollection<T> AsEnumerated<T>(this IEnumerable<T> original)
              {
                  if (original is IReadOnlyCollection<T> originalCollection)
                  {
                      return originalCollection;
                  }
      
                  return ImmutableArray.CreateRange(original);
              }
          }
      

      【讨论】:

        【解决方案9】:

        使用 .NET 6/C# 10

        .. 及以上您可以尝试使用Enumerable.TryGetNonEnumeratedCount(IEnumerable, Int32) 方法确定序列中元素的数量,而无需强制枚举。

        如果不用枚举就能确定source的计数,该方法返回true;否则,false。因此,您可以检查是否需要进一步实施。

        using System;
        using System.Collections.Generic;
        using System.Linq;
                            
        public class Program
        {
            public static void Main()
            {
                IEnumerable<int> arrayOne = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        
                var canGetCountDirectly = arrayOne.TryGetNonEnumeratedCount(out int theCount);
        
                Console.WriteLine($"Count can be returned directly = {canGetCountDirectly}");
                Console.WriteLine($"Count = {theCount}");
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-02-12
          • 1970-01-01
          • 1970-01-01
          • 2014-06-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多