Pull to refresh

Comments 46

Большинство разработчиков разумно предполагают, что результатом выполнения этого кода будет “1 2 3”
Я бы не сказал, что разумно. Скорее неразумно, ибо такое предположение можно сделать лишь при полном непонимании того, как работают замыкания.
Очень странное нововведение, а вдруг в существующем коде где-то использовалось замыкание на current в благих целях?
промахнулся :-(.
В текущем виде оно приводит к неопределённому поведению (точнее, поведение зависит от времени, когда будет выполнен код замыкания), так что не думаю, что его кто-то в здравом уме мог использовать рассчитывая на текущее поведение.
Нет никакого неопределённого поведения. Оно не зависит от времени. В любой момент у нас чёткая зависимость от значения переменной-итератора цикла.
В общем, я так и не увидел живого примера того, зачем надо замыкаться на переменную foreach-цикла в расчёте на то, что значение изменится. Пока что только придирки к формулировкам.
Несколько сотен постов на stackoverflow говорят о том, что очень-но многие разработчики считают именно «1 2 3» естественным результатом.

Естественно, после того, как вы узнали как это дело *устроено*, то все вопросы отпадают. Но ведь помимо нас с вами, есть еще пара миллионов индусов (это я про мировозрение, а не рассовую принадлежность), которые с нами не согласятся, поскольку они не знают да и не хотят знать, как это дело внутри устроено.
Ну я не спорю, что всё правильно сделали, просто такое поведение вытекает из общей логики работы циклов и замыканий.
Это вытекает из того, какое количество переменных существует в цикле foreach: одна на весь foreach или на каждую итерацию создается новая переменная.
Какому циклу первому учат в школе? for. В for переменная одна на весь цикл, есть явные конструкции управления её изменением и выходом из цикла. Следовательно, логичным было бы предположить, что в foreach так же, а конструкции скрыты от глаз (это если не читать стандарт ECMA, на одних предположениях из «логики вещей»). Логика тех, кто предполагает, что переменная каждый раз новая, мне не ясна.
А цикл for никак не изменился. В нем как была одна переменная на весь цикл, так и осталась.

Здесь был вопрос компромисса: что лучше, пожертвовать согласованностью с циклом for или устранить одно из самых назойливых непониманий языка C#. При создании C# 2.0 решили отдать предпочтение согласованности с циклом for, но со временем решили, что этой согласованностью стоит пожертвовать.
Эм. Где я сказал, что цикл for изменился?
Нет, нигде. Так, на всяк случай напомнил.

Мой (точнее разработчиков C#) rationale во втором абзаце.
Несколько сотен постов на stackoverflow говорят о том, что очень-но многие разработчики считают именно «1 2 3» естественным результатом.

Теперь будет полторы тысячи вопросов от тех же индусов — почему в foreach все работает «правильно», а в цикле for «мистика».
Zanuda mode=on
Индус — не расовая принадлежность, а вероисповедание. Индус — это приверженец индуизма. А расовая принадлежность это индиец.
Извините :)
Поскольку большинство разработчиков не представляют себе как работает .net внутри, то утверждение про разумность имеет смысл.
Эм. А ничего что переменная объявлена вне скобочной конструкции? Т. е. долна быть одна на все итерации того, что внутри фигурных скобок?
На мой взгляд «неправильный» код выглядит куда более естественно, чем код со временной переменной. Можно понимать что он неправильный, и чувствовать, что всё равно хотелось бы писать так. Тут Липперт молодец.
Вообще в C# еще несколько таких мест.
Мда… Мне как то изначальный вариант казался естественным… Это ж сломает обратную совместимость.
На самом деле не сломает. В цикле foreach всегда объявляется новая переменная. А поскольку раньше на неё никто не делал замыканий, то новая реализация ничего не сломает.
Ну Вы так ловко ответили прям за всех…
Ну приведите пример того, зачем нужно замыкаться на постоянно изменяющуюся переменную перечислителя цикла, рассчитывая при этом, что она будет изменена? Мне сложно представить ситуацию, когда такое может вообще понадобиться.
Впервые эту проблему поднял Эрик в конце 2009-го года и уже тогда обсуждались решения, как это можно пофиксить. Сейчас было принято решение, что нет никакого разумно корректного кода, который бы использовал эту фичу именно таким образом (т.е. чтобы намеренно замкнулся на переменную цикла, в надежде получить сотню одинаковых результатов).
Таким образом, в новой версии будет двойственное поведение переменных в такой конструкции:
var actions = new List<Action>();
var jj = 0;
foreach(var i in Enumerable.Range(1, 3))
{
    actions.Add(() => Console.WriteLine(i, jj));
    jj = jj +1;
}

foreach(var action in actions)
{
    action(); //даст для новой версии(5): 1 3   2 3   3 3
}

Скорее всего массовое непонимание усугубляет тот факт, что в цикле в этом языке создаётся окружение, а не в функции, как, например, в javascript и perl.

Но, впрочем, и замыкания в функциях в javascript по умолчанию никто не понимает, начинают понимать после того как напишут нечаянно замыкание, не зная об этом.
Поведение будет таким же «согласованным» как и ранее, но для этого нужно понимать две вещи: (1) экземпляр класса замыкания для каждой области видимости и (2) каждая итерация цикла foreach содержит новую переменную i.

В данном случае это означает, что для переменной jj будет создан один объект замыкания, который будет шарится всеми остальными объектами замыкания для каждой итерации.

Т.е. этот код можно рассматривать таким образом:

var actions = new List<Action>();
var jjClosure = new jjClosure();
jjClosure.jj = 0;

foreach(var i in Enumerable.Range(1, 3))
{
  // внутренний объект замыкания содержит ссылку на внешний объект замыкания
   var iClosure = new iClosure() {i = i, jjClosure = jjClosure};

   // () => Console.WriteLine(i, jj) теперь перехало в метод Action замыкания iClosure.
  // Тело iClosure.Action выглядит так:
  // Console.WriteLine(this.i, this.jjClosure.jj);
   actions.Add(iClosure.Action);
}



В результате чего мы и получим 1 3, 2 3, 3 3, поскольку всеми iClosure будет использоваться один и тот же объект jjClosure.
Эх, а ранее Липперт приводил вполне обоснованные аргументы, почему они не собираются делать это. Я лично и сам не знаю, как именно лучше — «чистота конструкции» или «интуитивная юзабельность в общем случае» — меня не напрягает то, как это есть сейчас. Да, про «Access to Modified Closure» знали не все. Видимо, Эрика всерьез достали с этим :)

По идее это не должно сломать существующий код, так что ужасного в этом решении ничего нет. Хотя личное ощущение, что это в чистом виде хак (да, не «синтэкс шуга», а именно хак).
UFO just landed and posted this here
Cегодня написал подобный код, но мне правда решарпер подсказал, что не все в порядке. обратил мое внимание на то, что я переменную цикла в замыкание засунул. Я заменил тело цикла на вызов функции (ну там передача по значению).
Вобщем будет удобнее в C#5.
Эта фича будет «компилироваться» только в .net 4.5?

А то если будет в .net 4, то возможна ситуация, что один и тот же код скомпиленный в VS2010 (со старым поведением) не работает, а в VS11 — работает. Совместное использование студий будет тогда проблематично.
Это фича именно компилятора языка C# 5.0, причем не важно, на какую платформу мы таргетимся, т.е. при запуске этого кода из VS11 будет результат «1 2 3», даже если мы таргетимся на .NET Framework 3.5.

Поэтому ситуация, когда из VS10 будет один результат, а из VS11 — другой, вполне возможна. В данном случае решением является использования «общего» подхода, которое подойдет для всех версий — т.е. явное использование темповой переменной внутри цикла.
В Mono 2.10.5 результат — 3, 3, 3

Чую — путаница будет с этими замыканиями…

Но идея мне кажется здравой — всегда подсознательно не понимал почему в циклах «int» пишут в скобочках, а не где-нибудь еще…

Если введут эту фичу и для for, то в моей голове «все сложится»

Старое поведение:
int i;
for( i=0; i<10; i++ )
{
  /* Здесь работаем с одной и той же переменной */
}

Новое поведение:
for( int i=0; i<10; i++ )
{
  /* А здесь для каждой итерации генерируется новая переменная */
}

Осталось только дождаться когда Липперт соберется с духом и возьмет на себя ответственность за обратную совместимость… :)
Переменную цикла пишут в скобочках, потому что она локальная для этого цикла: дело не в том, одна она или на каждый чих новая, а в области видимости. Вне цикла её не должно быть
Спасибо за напоминание — это я усвоил, когда изучал циклы.
И даже могу сказать почему этой фичи никогда не будет в for — все дело в обратной совместимости с такими конструкциями:
for( int i=0; i<10; i++ )
{
  if( true ) i+=3;
}
Вот только внутри if должно стоять не true, а более сложное условие.
Ответьте мне, пожалуйста, каким образом конструкция i++ может быть применима к переменной, генерируемой для каждой итерации заново?
Нда — неправильно в комментариях мысль написал. Надо было писать:

Новое поведение:
for( int i=0; i<10; i++ )
{
  /* А здесь для каждой итерации создается новый экземпляр Closure, что приводит к сохранению нужного значения переменной i */
}
</sorce>

for (int i=0; i<10; i++)
{
  Action act = () => i--; // Так будет делать нельзя - переменная i стала вдруг привязанной к итерации
  i--; // а так по-прежнему можно?
}


Вам это странным не кажется?
Хм…
А как Ваш пример работает в цикле foreach?
Неужели мы получили еще одни грабли?
А цикле foreach мой пример не работает.
Я не могу придумать ни одной ситуации, в которой понадобилось бы изменять переменную цикла.

Потому в новой версии языка foreach и переделали, а for оставили.
А для чего вообще сделали такое поведение, как в 4.0? Это концепция или побочный эффект?
Побочный эффект на пересечении двух концепций.

Читайте статью внимательнее.
Мне жутко не нравится то, что если код, написанный на C# 5 (не нужны временные переменные) откомпилировать на C# 4 (нужны временные переменные) то будет непредсказуемое поведение :(
а зачем это делать?
в общем случае код просто не скомпилируется.
Спасибо за статью. Вообще в первый раз услышал про замыкания.
Странно, что об этом не написал Рихтер)
На самом деле, у Рихтера об этом все написано, включая магию генерации классов замыканий при захвате внешних переменных из анонимного метода. Просто страшных терминов, типа closure у него нет.

См. главу 17. Delegates раздел Syntactical Shortcut #3: No Need to Wrap Local Variables in a Class Manually to Pass Them to a Callback Method
Отличная статья, спасибо! Жаль, что разработчики C# идут на поводу горе-программистов… Для меня лично раньше всё было очевидно, а сейчас уже нет. Тем более, что for остался прежним. Такой подход идёт вразрез с логикой…
Sign up to leave a comment.

Articles