Замыкания на переменных цикла. Часть 2

Спасибо всем, кто оставил содержательные и глубокие комментарии для предыдущего сообщения[1].

Большинству стран действительно стоит применить методы «мажоритарно-преференциального[2]» голосования. Эта же система лучше подходит гикам. Многие оставили свои мнения в форме: «Я бы хотел, чтобы вы внесли изменения, но если не можете, тогда выдавайте предупреждения». Или «Не вносите изменения, просто добавьте предупреждения» и т.д. Но читая комментарии, мне стало совершенно очевидно отсутствие единого мнения о том, что же лучше предпринять в этом случае. Я выполнил некоторые расчеты:

  • Поддерживают предупреждение: 26 человек
  • Выражают мнение, что «лучше ничего не менять»: 26 человек
  • Выражают мнение, что «лучше внести изменение»: 25 человек

Ух ты! Придется бросать монетку. :-) (*)

Четверо выразило мнение, что внесение такого изменения было бы ошибкой. Это довольно серьезные изменения, особенно потому, что они поломают не только «неработающий» код, но и большое количество кода, который сегодня прекрасно работает (см. ниже). Вряд ли такое случится.

Кроме того, многие оставили интересные советы, некоторые из которых я бы хотел обсудить.

Прежде всего, хочется подчеркнуть, что обсуждаемая проблема заключается в том, что язык способствует написанию кода, поведение которого отличается от предполагаемого. И проблема заключается НЕ В ТОМ, что язык не позволяет выразить необходимое поведение (он это позволяет). Для этого всего лишь нужно явно добавить переменную внутрь цикла.

Некоторые советы сводились к тому, чтобы язык мог бы элегантнее выражать эту идею. Вот некоторые предложения:

foreach(var x in c) inner
foreachnew(var x in c)
foreach(new var x in c)
foreach(var x from c)
foreach(var x inside c)

Хотя мы можем реализовать любой из этих вариантов, ни один из них не решает настоящую проблему. Сегодня вы должны знать конкретный способ применения цикла foreach, чтобы добиться требуемого поведения: объявить переменную внутри цикла. С внесением одного из этих изменений, вам все еще придется помнить о конкретном ключевом слове для получения требуемой семантики. Но все еще довольно просто случайно воспользоваться не тем вариантом.

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

Светило С++, Герб Саттер, недавно был в городе и заехал в мой офис, чтобы рассказать, как подобная проблема решается в С++. Скорее всего, в новой версии стандарта С++ будут включены лямбда-выражения. Они поступили следующим образом:

[ q, & r] ( int x) -> int { return M(x, q, r); }

Это означает, что лямбда-выражение захватывает внешнюю переменную q по значению, а переменную r – по ссылке. Лямбда-выражение принимает int в качестве параметра и возвращает int. Так контролируется способ захвата внешней переменной лямбда-выражением: либо по ссылке, либо по значению! Интересный подход, но он не решает нашу проблему. Без нарушения работы существующего кода мы не можем захватывать переменную лямбда-выражением по значению. Захват по значению потребует нового синтаксиса, что в результате снова приведет к той же самой проблеме: пользователь должен знать о необходимости применения нового синтаксиса в циклах foreach.

Некоторые люди также задавали вопрос о том, а какие минусы в добавлении предупреждения. А минус заключается в том, что это очень плохое предупреждение компилятора, которое предупреждает о корректном поведении. Это заставляет людей исправлять работающий код, что приводит к появлению ошибок в процессе устранения предупреждения, которого вообще быть не должно. Рассмотрим следующий пример:

foreach(var insect in insects)
{
  var query = frogs.Where(frog=>frog.Eats(insect));
  Console.WriteLine("{0} is eaten by {1} frogs.", insect, query.Count());
}

В этом случае происходит замыкание лямбда-выражения на переменную insect. В этом случае нет никакой проблемы, т.к. лямбда-выражение не выходит за пределы цикла. Но компилятор не знает об этом. Компилятор видит, что лямбда-выражение передается методу Where, а этот метод может делать с ним все что угодно, даже сохранить для последующих вызовов. Именно это и делается в методе Where! Метод Where сохраняет лямбду в монаде, которая представляет выполнение запроса. Тот факт, что объект запроса не выходит за пределы цикла делает этот код безопасным. Но как компилятору понять это? Нам придется выдавать предупреждение в этом случае, хотя код совершенно безопасен.

Хуже того. Во многих организациях является требованием компилировать проект с выставленным флажком “warnings are errors”. Поэтому каждый раз, добавляя предупреждение для распространенного случая, который чаще всего является безопасным, мы допускаем серьезное нарушение. Сложно назвать удачным лекарство, которое убивает больше людей, чем сама зараза (**).

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

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

И, наконец, некоторые предлагали «сделать предупреждением в C# 4 и ошибкой в C# 5» или что-то в таком духе. К вашему сведению, C# 4 УЖЕ ГОТОВ. Мы лишь исправляем «убойные» ошибки, в основном после отзывов, полученных при использовании бета-версии. (Если вы все еще находите ошибки в бета-версии, пожалуйста, продолжайте присылать их, хотя вряд ли они будут исправлены до выпуска.) В это время мы точно не можем вносить серьезные изменения в дизайн языка или добавлять новые возможности. И мы постараемся не вносить семантические изменения или новые возможности в пакетах исправлений. К сожалению, нам придется жить с этой проблемой, по крайней мере, до следующего выпуска.

**********

(*) Смайлик с улыбкой говорит о том, что Эрик не отказывает себе в возможности пошутить.

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


[1] Речь идет о комментариях, оставленных к оригинальной версии сообщения, расположенного по адресу: https://blogs.msdn.com/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx. - Примеч. перев.

[2] Мажоритарно-преференциальное или альтернативное голосование (Instant runoff voting или Alternative voting) – система голосования, когда каждый бюллетень содержит полный список кандидатов, и каждый избиратель ранжирует их в порядке своего предпочтения. В самом распространённом формате используются числа по возрастанию, когда избиратель ставит «1» напротив имени самого желательного кандидата, «2» — напротив второго по предпочтительности, и так далее. – Примеч. перев.

Оригинальное сообщение: Closing over the loop variable, part two