关于c#:处理可能多次枚举IEnumerable的警告

Handling warning for possible multiple enumeration of IEnumerable

在我的代码中需要多次使用一个IEnumerable<>,从而得到"IEnumerable的可能多次枚举"的再harper错误。

样例代码:

1
2
3
4
5
6
7
8
9
10
11
12
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
1
2
3
4
5
 public List<object> Foo(IEnumerable<object> objects)
 {
    var objectList = objects.ToList();
    // ...
 }

但这很尴尬。

在这种情况下,您会怎么做?


IEnumerable作为参数的问题是它告诉调用方"我想枚举这个"。它没有告诉他们你想列举多少次。

I can change the objects parameter to be List and then avoid the possible multiple enumeration but then I don't get the highest object that I can handle.

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

这里缺少的语义是,调用方可能不花时间阅读方法的详细信息,可能会假定您只迭代一次,因此它们会传递给您一个昂贵的对象。您的方法签名没有指明任何一种方式。

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

否则,大多数研究该方法的开发人员可能会假定您只迭代一次。如果服用IEnumerable是如此重要,您应该考虑在方法开始时使用.ToList()

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


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

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

注意,我稍微改变了剂量的语义,但这主要是为了显示未展开的用法。例如,您可以重新包装迭代器。您也可以将它设置为一个迭代器块,这很好;然后就没有list—您可以在得到这些项时yield return它们,而不是添加到要返回的列表中。


在方法签名中使用IReadOnlyCollectionIReadOnlyList,而不是IEnumerable,有一个优点,即在迭代之前可能需要检查计数,或者由于其他原因重复多次。

但是,它们有一个巨大的缺点,如果您试图重构代码以使用接口,例如使其更易于测试和使用动态代理,则会导致问题。关键是IList不继承IReadOnlyList的数据,其他集合及其各自的只读接口也不继承。(简而言之,这是因为.NET 4.5希望与早期版本保持ABI兼容性。但他们甚至没有利用这个机会在.NET核心中改变这一点。)

这意味着,如果你从程序的某个部分得到一个IList,并且想把它传递给另一个期望得到IReadOnlyList的部分,你就不能!但是,您可以将IList作为IEnumerable传递。

最后,IEnumerable是所有.NET集合(包括所有集合接口)支持的唯一只读接口。当你意识到自己被某些体系结构选择拒之门外时,任何其他的选择都会回来咬你。所以我认为在函数签名中使用它来表示您只需要一个只读集合是合适的类型。

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

和往常一样,这不是绝对的,如果您编写的是数据库重代码,其中意外的多个枚举将是一个灾难,那么您可能更喜欢另一种权衡。


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

1
2
3
4
5
6
7
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重载来处理这个问题。


如果目的真的是防止多次枚举,而不是Marc Gravell的答案是要读取的,但是保持相同的语义,您可以简单地删除多余的AnyFirst调用,然后继续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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不是泛型的,或者至少被约束为引用类型。


如果只需要检查第一个元素,则可以在不重复整个集合的情况下查看它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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;
            }
        }
    }
}


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

在这个场景中,您还可以进一步利用强大的LINQ扩展方法。

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

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