Handling warning for possible multiple enumeration of IEnumerable
在我的代码中需要多次使用一个
样例代码:
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(); // ... } |
但这很尴尬。
在这种情况下,您会怎么做?
把
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查询传递给此方法,只为您枚举两次(每次都可能获得不同的结果?)
这里缺少的语义是,调用方可能不花时间阅读方法的详细信息,可能会假定您只迭代一次,因此它们会传递给您一个昂贵的对象。您的方法签名没有指明任何一种方式。
通过将方法签名更改为
否则,大多数研究该方法的开发人员可能会假定您只迭代一次。如果服用
很遗憾.NET没有IEnumerable+Count+索引器接口,没有添加/删除等方法,这是我怀疑可以解决此问题的方法。
如果您的数据总是可以重复的,也许不用担心。但是,您也可以展开它-如果传入数据可能很大(例如,从磁盘/网络读取),这尤其有用:
1 2 3 4 5 6 7 8 9 10 11 12 |
注意,我稍微改变了剂量的语义,但这主要是为了显示未展开的用法。例如,您可以重新包装迭代器。您也可以将它设置为一个迭代器块,这很好;然后就没有
在方法签名中使用
但是,它们有一个巨大的缺点,如果您试图重构代码以使用接口,例如使其更易于测试和使用动态代理,则会导致问题。关键是
这意味着,如果你从程序的某个部分得到一个
最后,
(请注意,如果基础类型支持两个接口,则可以编写一个简单的强制转换的
和往常一样,这不是绝对的,如果您编写的是数据库重代码,其中意外的多个枚举将是一个灾难,那么您可能更喜欢另一种权衡。
在这种情况下,我通常使用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的答案是要读取的,但是保持相同的语义,您可以简单地删除多余的
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; } |
注意,这假定您
如果只需要检查第一个元素,则可以在不重复整个集合的情况下查看它:
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; } } } } |
首先,这个警告并不总是那么重要。我通常在确认它不是性能瓶颈后禁用它。这仅仅意味着
在这个场景中,您还可以进一步利用强大的LINQ扩展方法。
1 2 | var firstObject = objects.First(); return DoSomeThing(firstObject).Concat(DoSomeThingElse(objects).ToList(); |
在这种情况下,可能只评估一次