在我需要多次使用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();
// ...
}
但这只是尴尬。
在这种情况下你会做什么?
答案 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的答案是要阅读的,但保持相同的语义,您可以简单地删除多余的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
不是通用的,或者至少被限制为引用类型。
答案 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
一次,但首先是个人资料并查看它是否确实存在问题。