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

时间:2011-11-23 10:47:55

标签: c# .net performance resharper

在我需要多次使用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();
    // ...
 }

但这只是尴尬

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

7 个答案:

答案 0 :(得分:442)

IEnumerable作为参数的问题在于它告诉来电者“我希望枚举这个”。它没有告诉他们你想要枚举多少次。

  

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

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

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

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

否则,大多数查看该方法的开发人员可能会假设您只迭代一次。如果选择IEnumerable非常重要,则应考虑在方法开始时执行.ToList()

很遗憾.NET没有IEnumerable + Count + Indexer的接口,没有Add / Remove等方法,这是我怀疑会解决这个问题的方法。

答案 1 :(得分:29)

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

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项目,而不是添加到要返回的列表中。

答案 2 :(得分: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不是通用的,或者至少被限制为引用类型。

答案 3 :(得分:4)

在这种情况下,我通常使用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重载处理它。

答案 4 :(得分:4)

在方法签名中使用IReadOnlyCollection<T>IReadOnlyList<T>而不是IEnumerable<T>的好处是可以明确地表明您可能需要在迭代之前检查计数,或多次迭代其他原因。

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

这意味着,如果您从程序的某个部分获得了IList<T>,并且希望将其传递给期望IReadOnlyList<T>的另一部分,那么您将无法! 不过,您可以将IList<T>作为IEnumerable<T>

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

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

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

答案 5 :(得分:0)

如果只需要检查第一个元素,则可以在不迭代整个集合的情况下窥视它:

^[^b]*b:3

答案 6 :(得分:-2)

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

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

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

在这种情况下,只能用一些麻烦来评估IEnumerable一次,但首先是个人资料并查看它是否确实存在问题。