我正在阅读一本C#书,其中作者(一些名叫Jon Skeet的家伙)实现了Where
函数,如
public static IEnumerable<T> Where<T> ( this IEnumerable<T> source, Funct<T,bool> predicate )
{
if ( source == null || predicate == null )
{
throw new ArgumentNullException();
}
return WhereImpl(source, predicate);
}
public static IEnumerable<T> WhereImpl<T> ( IEnumerable <T> source, Func<T,bool> predicate )
{
foreach ( T item in source )
{
if ( predicate(item) )
{
yield return item;
}
}
}
现在,我完全理解这是如何工作的,它等同于
public static IEnumerable<T> Where<T> ( this IEnumerable<T> source, Funct<T,bool> predicate )
{
if ( source == null || predicate == null )
{
throw new ArgumentNullException();
}
foreach ( T item in source )
{
if ( predicate(item) )
{
yield return item;
}
}
}
提出了为什么会将这些分成2个函数的问题,因为会有内存/时间开销,当然还有更多的代码。我总是验证参数,如果我像这个例子一样开始编写,那么我将编写两倍的代码。是否有一些思想认为验证和实施应该是单独的功能?
答案 0 :(得分:15)
原因是迭代器块总是很懒惰。除非您调用GetEnumerator()
然后调用MoveNext()
,否则方法中的代码将无法执行。
换句话说,请考虑调用您的“等效”方法:
var ignored = OtherEnumerable.Where<string>(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
,然后,将抛出异常,但这可能不是您调用该方法的位置。在大多数情况下这没关系,但值得了解。
答案 1 :(得分:5)
这可能取决于场景和您的编码风格。当您使用yield
创建迭代器时,Jon Skeet绝对正确地说明为什么它们应该分开。
前置条件不是迭代器块的一部分,因此,如果不满足整个前置条件,则以下代码将立即抛出合同异常:
public static class Test
{
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> 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<string>(null, null);
答案 2 :(得分:1)
使用yield return
的方法看起来非常简单,但是如果你检查编译后的代码,你会发现它变得非常复杂。
编译器使用状态机逻辑为您生成一个新类,以支持枚举。对于第二个Where
方法,在反编译后大约有160行代码。实际的Where
方法编译为
[IteratorStateMachine(typeof(IterarorTest.<Where>d__0<>))]
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
IterarorTest.<Where>d__0<T> expr_07 = new IterarorTest.<Where>d__0<T>(-2);
expr_07.<>3__source = source;
expr_07.<>3__predicate = predicate;
return expr_07;
}
如您所见,此方法中未检查任何参数。它只返回一个新的迭代器。
在自动生成的类'MoveNext
方法中检查参数(代码有点太长,无法在此处发布)。
另一方面,如果将yield return
移动到另一个方法,则在调用Where
方法时会立即检查参数 - 这是此处的预期行为。
修改强>
作为noticed by Matias Fidemraizer,代码合同也解决了问题 - 合同检查已插入Where
方法
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
__ContractsRuntime.Requires(source != null, null, "source != null");
__ContractsRuntime.Requires(predicate != null, null, "predicate != null");
IterarorTest.<Where>d__0<T> expr_27 = new IterarorTest.<Where>d__0<T>(-2);
expr_27.<>3__source = source;
expr_27.<>3__predicate = predicate;
return expr_27;
}