我正在阅读一本C#书,其中作者(一些名叫Jon Skeet的家伙)实现了Where
类似的功能
public static IEnumerableWhere ( this IEnumerable source, Funct predicate ) { if ( source == null || predicate == null ) { throw new ArgumentNullException(); } return WhereImpl(source, predicate); } public static IEnumerable WhereImpl ( IEnumerable source, Func predicate ) { foreach ( T item in source ) { if ( predicate(item) ) { yield return item; } } }
现在,我完全理解这是如何工作的,它相当于
public static IEnumerableWhere ( this IEnumerable source, Funct predicate ) { if ( source == null || predicate == null ) { throw new ArgumentNullException(); } foreach ( T item in source ) { if ( predicate(item) ) { yield return item; } } }
这就提出了一个问题,即为什么会将这些函数分成2个函数,因为会有内存/时间开销,当然还有更多的代码.我总是验证参数,如果我开始写这个例子,那么我将编写两倍的代码.是否有一些思想认为验证和实施应该是单独的功能?
原因是迭代器块总是很懒惰.除非你调用GetEnumerator()
然后MoveNext()
,方法中的代码将不会被执行.
换句话说,考虑这种对"等效"方法的调用:
var ignored = OtherEnumerable.Where(null, null);
没有异常被抛出,因为你没有打电话GetEnumerator()
然后MoveNext()
.将其与我的版本进行比较,无论返回值如何使用,都会立即抛出异常...因为它只是在热切地验证之后才使用迭代器块调用该方法.
请注意,async/await具有类似的问题 - 如果您有:
public async Task FooAsync(string x) { if (x == null) { throw new ArgumentNullException(nameof(x)); } // Do some stuff including awaiting }
如果你打电话给这个,你最终会得到一个错误Task
- 而不是NullReferenceException
被抛出.如果等待返回Task
,则抛出异常,但这可能不是您调用方法的地方.在大多数情况下这没关系,但值得了解.
它可能取决于场景和您的编码风格.当您使用yield
创建迭代器时,Jon Skeet绝对正确地说明为什么它们应该分开.
顺便说一句,我认为在这里添加我的两分钱可能很有意思:使用代码合同(即按合同设计)的相同代码以不同的方式运行.
前置条件不是迭代器块的一部分,因此,如果不满足整个前置条件,则以下代码将立即抛出合同异常:
public static class Test { public static IEnumerableWhere (this IEnumerable source, Func predicate) { Contract.Requires(source != null); Contract.Requires(predicate != null); foreach (T item in source) { if (predicate(item)) { yield return item; } } } } // This throws a contract exception directly, no need of // enumerating the returned enumerable Test.Where (null, null);