Подтвердить что ты не робот

Отдельные функции в валидации и реализации? Зачем?

Я читал книгу С#, в которой автор (какой-то чувак по имени Джон Скит) реализует функцию 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;
      }
    }
}

в котором возникает вопрос, почему можно отделить их от двух функций, учитывая, что накладные расходы на память/время и, конечно, больше кода. Я всегда проверяю параметры, и если я начну писать, как в этом примере, я буду писать в два раза больше кода. Есть ли какая-то школа мысли, которая утверждает, что валидация и реализация должны быть отдельными функциями?

4b9b3361

Ответ 1

Причина в том, что блок итератора всегда ленив. Если вы не вызываете 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, тогда будет выбрано исключение, но это может быть не тот метод, который вы назвали. Это нормально в большинстве случаев, но стоит знать.

Ответ 2

Это может зависеть от сценария и стиля кодирования. Jon Skeet абсолютно прав, почему они должны быть разделены, когда вы используете yield для создания итераторов.

Кстати, я подумал, что может быть интересно добавить мои два цента здесь: тот же код с использованием Code Contracts (т.е. дизайн по контракту) ведет себя по-другому.

Предварительные условия не являются частью блока итератора, поэтому следующий код сразу же выдает исключение контракта, если все предварительные условия не выполняются:

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);

Ответ 3

Метод с использованием 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;
}

Как вы можете видеть, в этом методе не проверяется никаких аргументов. Он просто возвращает новый итератор.

Аргументы проверяются в методе auto-generated class 'MoveNext (код слишком длинный для публикации здесь).

С другой стороны, если вы переместите yield return на другой метод, аргументы будут немедленно проверены, когда вы вызываете метод Where - это ожидаемое поведение здесь.

Edit

Как отмеченный Матиасом Фидемаризером, кодовые контракты также решают проблему - проверки контракта вставляются в метод 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;
}