在我的代码中,我需要使用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;
}
我可以将对象参数改为List,然后避免可能的多重枚举,但这样我就得不到我能处理的最高的对象。
我能做的另一件事是在方法的开头将IEnumerable转换为List:
public List<object> Foo(IEnumerable<object> objects)
{
var objectList = objects.ToList();
// ...
}
但这太尴尬了。
在这种情况下你会怎么做?
在方法签名中使用IReadOnlyCollection<T>或IReadOnlyList<T>而不是IEnumerable<T>,可以显式地表明您可能需要在迭代之前检查计数,或者由于其他原因迭代多次。
然而,它们有一个巨大的缺点,如果你试图重构你的代码来使用接口,比如让它更可测试,更适合动态代理,就会导致问题。关键是IList<T>不继承IReadOnlyList<T>,其他集合及其各自的只读接口也是如此。(简而言之,这是因为。net 4.5想要保持ABI与早期版本的兼容性。但是他们甚至没有抓住机会在。net核心中改变这一点。)
这意味着如果你从程序的某些部分获得一个IList<T>,并且想要将它传递给另一个需要IReadOnlyList<T>的部分,你不能!但是,您可以将IList<T>作为IEnumerable<T>传递。
最后,IEnumerable<T>是所有。net集合(包括所有集合接口)支持的唯一只读接口。当您意识到您将自己锁在一些架构选择之外时,任何其他选择都会反过来咬您一口。所以我认为在函数签名中使用它是合适的类型来表示你只想要一个只读集合。
(注意,如果底层类型支持两个接口,你总是可以写一个IReadOnlyList<T> ToReadOnly<T>(这个IList<T>列表)扩展方法来简单地强制转换,但是当重构时,你必须手动在所有地方添加它,其中IEnumerable<T>总是兼容的。)
这并不是绝对的,如果您正在编写大量数据库的代码,而意外的多重枚举将是一场灾难,那么您可能更喜欢不同的权衡。
如果您的数据总是可重复的,也许就不用担心了。但是,你也可以展开它——如果传入的数据可能很大(例如,从磁盘/网络读取),这特别有用:
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的语义,但这主要是为了显示展开的用法。例如,您可以重新包装迭代器。你也可以把它变成一个迭代器块,这样很好;然后就没有列表了——你会在得到它们的时候就返回它们,而不是添加到要返回的列表中。
在这种情况下,我通常用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重载来处理。
如果目的真的是为了防止多重枚举,那么Marc Gravell的答案是值得阅读的,但保持相同的语义,你可以简单地删除冗余的Any和First调用,并使用:
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不是泛型的,或者至少被限制为引用类型。