2015-12-09 4 views
10

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

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

ответ

14

Причина в том, что блок итератора всегда ленив. Если вы не вызываете GetEnumerator(), а затем MoveNext(), код в методе не будет выполнен.

Других слов, рассмотрит этот вызов на «эквивалентную» методу:

var ignored = OtherEnumerable.Where<string>(null, null); 

Исключения не отбрасываются, потому что вы не вызывая GetEnumerator(), а затем MoveNext(). Сравните это с моей версией, в которой исключение выбрано сразу независимо от того, как используется возвращаемое значение ... потому что он вызывает только метод с блоком итератора после с правильной проверкой.

Обратите внимание, что асинхронный/Await имеет схожие проблемы - если у вас есть:

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

Если назвать это, вы в конечном итоге получить нарушенные Task - а не NullReferenceException бросают. Если вы ожидаете возвращения Task, , то будет выбрано исключение, но это может быть не так, как вы вызвали этот метод. В большинстве случаев это нормально, но стоит знать.

+1

BTW используя * код * контракты, возможно, не была бы реальная причина чтобы отделить их двумя способами, поскольку проверка параметров будет выполняться с использованием предварительных условий ... Я не могу проверить это сейчас, но я не уверен, что * кодовые контракты * также будут оцениваться как часть блока итератора –

+1

Ну, наконец, я сам это испытал, мне было любопытно о сценарии, который я сказал вам в вышеупомянутом комментарии. Я отправил свой собственный ответ, чтобы внести вклад в это исключение из правила –

5

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

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

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

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); 
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; 
} 
Смежные вопросы