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

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

示例代码:

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();
// ...
}

但这只是尴尬的

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

158076 次浏览

IEnumerable作为参数的问题是,它告诉调用者“我希望枚举这个”。它不会告诉他们你希望列举多少次。

我可以将对象参数更改为List,然后避免可能的多重枚举,但这样我就不会得到我能处理的最高的物体

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

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

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

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

遗憾的是。net没有IEnumerable + Count + Indexer的接口,没有Add/Remove等方法,这是我认为可以解决这个问题的方法。

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

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

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

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

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

在这种情况下,可能只计算IEnumerable一次,但会有一些麻烦,但先进行配置,看看它是否真的是一个问题。

如果目的真的是为了防止多重枚举,那么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不是泛型,或者至少被约束为引用类型。

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

在方法签名中使用IReadOnlyCollection<T>IReadOnlyList<T>而不是IEnumerable<T>,其优点是显式地表明在迭代之前可能需要检查计数,或者由于其他原因迭代多次。

然而,它们有一个巨大的缺点,如果你试图重构你的代码来使用接口,比如让它更可测试,更适合动态代理,就会导致问题。关键是__ABC0不继承IReadOnlyList<T>,其他集合及其各自的只读接口也是如此。(简而言之,这是因为。net 4.5想要保持ABI与早期版本的兼容性。但是他们甚至没有抓住机会在。net核心中改变这一点。)

这意味着如果你从程序的某些部分获得IList<T>,并想将它传递给另一个需要IReadOnlyList<T>的部分,你不能!但是,您可以将__ABC0作为IEnumerable<T>传递

最后,IEnumerable<T>是所有。net集合(包括所有集合接口)支持的唯一只读接口。当您意识到您将自己锁在一些架构选择之外时,任何其他选择都会反过来咬您一口。所以我认为在函数签名中使用它是合适的类型来表示你只想要一个只读集合。

(注意,如果底层类型支持两种接口,则始终可以编写IReadOnlyList<T> ToReadOnly<T>(this IList<T> list)扩展方法进行简单类型转换,但在重构时必须手动将其添加到所有位置,其中IEnumerable<T>始终是兼容的。)

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

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

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;
}
}
}
}

如果需要多次枚举某个对象,我喜欢使用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);
}
}

.NET 6/ c# 10

.. 除此之外,可以尝试使用可列举的。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}");
}
}