将功能分为验证和实施?为什么呢?

时间:2015-12-09 19:00:23

标签: c# design-patterns

我正在阅读一本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个函数的问题,因为会有内存/时间开销,当然还有更多的代码。我总是验证参数,如果我像这个例子一样开始编写,那么我将编写两倍的代码。是否有一些思想认为验证和实施应该是单独的功能?

3 个答案:

答案 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;
}