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

Sub*_*Dev 11 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;
      }
    }

}
Run Code Online (Sandbox Code Playgroud)

现在,我完全理解这是如何工作的,它相当于

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;
      }
    }
}
Run Code Online (Sandbox Code Playgroud)

这就提出了一个问题,即为什么会将这些函数分成2个函数,因为会有内存/时间开销,当然还有更多的代码.我总是验证参数,如果我开始写这个例子,那么我将编写两倍的代码.是否有一些思想认为验证和实施应该是单独的功能?

Jon*_*eet 15

原因是迭代器块总是很懒惰.除非你调用GetEnumerator()然后MoveNext(),方法中的代码将不会被执行.

换句话说,考虑这种对"等效"方法的调用:

var ignored = OtherEnumerable.Where<string>(null, null);
Run Code Online (Sandbox Code Playgroud)

没有异常被抛出,因为你没有打电话GetEnumerator()然后MoveNext().将其与我的版本进行比较,无论返回值如何使用,都会立即抛出异常...因为它只是热切地验证之后才使用迭代器块调用该方法.

请注意,async/await具有类似的问题 - 如果您有:

public async Task FooAsync(string x)
{
    if (x == null)
    {
        throw new ArgumentNullException(nameof(x));
    }
    // Do some stuff including awaiting
}
Run Code Online (Sandbox Code Playgroud)

如果你打电话给这个,你最终会得到一个错误Task- 而不是NullReferenceException被抛出.如果等待返回Task,抛出异常,但这可能不是您调用方法的地方.在大多数情况下这没关系,但值得了解.


Mat*_*zer 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);
Run Code Online (Sandbox Code Playgroud)