处理可能多次枚举IEnumerable的警告

gdo*_*ica 339 .net c# resharper performance

在我的代码中需要使用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;
}
Run Code Online (Sandbox Code Playgroud)
  • 我可以更改objects参数List,然后避免可能的多次枚举,但后来我没有得到我能处理的最高对象.
  • 我可以做的另一件事是将转换IEnumerableList在方法的开头:

 public List<object> Foo(IEnumerable<object> objects)
 {
    var objectList = objects.ToList();
    // ...
 }
Run Code Online (Sandbox Code Playgroud)

但这只是尴尬.

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

Pau*_*ell 454

IEnumerable作为参数的问题在于它告诉呼叫者"我希望枚举这个".它没有告诉他们你想要枚举多少次.

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

采取最高目标的目标是高尚的,但它为太多的假设留下了空间.你真的希望有人将LINQ to SQL查询传递给这个方法,只为你枚举它两次(每次得到可能不同的结果吗?)

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

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

否则,大多数查看该方法的开发人员可能会假设您只迭代一次.如果采取一个IEnumerable非常重要的,你应该考虑.ToList()在方法的开头做.

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

  • @DanNeely我建议[`IReadOnlyCollection(T)`](http://msdn.microsoft.com/en-us/library/hh881542)(新的.net 4.5)作为传达它的想法的最佳界面一个`IEnumerable(T)`,它被多次枚举.正如这个答案所述,"IEnumerable(T)"本身是如此通用,它甚至可以引用不可重置的内容,这些内容在没有副作用的情况下无法再次枚举.但是`IReadOnlyCollection(T)`意味着可重复使用. (68认同)
  • ReadOnlyCollection <T>是否满足您所需的接口要求?http://msdn.microsoft.com/en-us/library/ms132474.aspx (30认同)
  • 为什么在 Resharper 中采用“IList”而不是“IEnumerable”会消除此分析警告?通常的做法是采用最开放/通用的类型;所以我不知道“IList”如何解决潜在的多次迭代问题。整个警告让我感到非常困惑。 (3认同)

Mar*_*ell 30

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

  • @gdoron的问题,你明确表示你真的不希望这样做,P此外,ToList()方法,如果该数据,如果非常大的(潜在的无限可能不适合 - 序列并不需要是有限的,但可以仍然是迭代).最后,我只是提出一个选择.只有您知道完整的上下文才能确定是否有必要.如果您的数据是可重复的,另一个有效选项是:不要改变任何东西!忽略R#而不是......这一切都取决于上下文. (10认同)
  • @gdoron不是一切都很漂亮;但我不知道上面的内容是不可读的.特别是如果它被制成一个迭代器块 - 非常可爱,IMO. (5认同)
  • +1那么这是一个非常好的代码,但是 - 我放弃了代码的漂亮和可读性. (3认同)
  • @anatol `IEnumerable&lt;T&gt;` 只是意味着“一个序列”;如果该序列来自内存中的列表/数组/等,那么可以肯定:它可能是可重复的 - 但是 - 它可能从套接字、PRNG 或大量其他地方提取数据;从根本上讲,不能保证 IEnumerable&lt;T&gt; 是可重复的 (2认同)

Nis*_*nga 11

使用 .NET 6/C# 10

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

如果无需枚举即可确定 的true计数,则此方法返回;source否则,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}");
    }
}
Run Code Online (Sandbox Code Playgroud)


Gab*_*rin 6

在方法签名中使用IReadOnlyCollection<T>IReadOnlyList<T>来代替IEnumerable<T>,其优点是可以明确表明您可能需要在进行迭代之前检查计数,或出于某些其他原因而进行多次迭代。

但是,它们有很大的缺点,如果您尝试将代码重构为使用接口,例如会使其对动态代理更具可测试性和友好性,则会导致问题。关键点是它IList<T>不继承自IReadOnlyList<T>,并且对于其他集合及其各自的只读接口也类似。(简而言之,这是因为.NET 4.5希望保持与早期版本的ABI兼容性。但是他们甚至没有抓住机会在.NET Core中进行更改。

这意味着,如果您IList<T>从程序的某个部分得到一个并将其传递给期望一个的另一部分,IReadOnlyList<T>则不能!不过,您可以将传递IList<T>IEnumerable<T>

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

(请注意,IReadOnlyList<T> ToReadOnly<T>(this IList<T> list)如果基础类型同时支持两个接口,则始终可以编写一个简单的转换扩展方法,但是在重构时,IEnumerable<T>必须始终在任意位置手动添加它,因为它们始终兼容。)

与往常一样,这不是绝对的,如果您正在编写大量数据库的代码,而意外的多次枚举将成为灾难,那么您可能会希望选择其他折衷方案。

  • +1 用于发布旧帖子并添加有关使用 .NET 平台提供的新功能(即 IReadOnlyCollection&lt;T&gt;)解决此问题的新方法的文档 (5认同)