Как стать автором
Обновить

Комментарии 43

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

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


P.S.
Енумератор режет глаз. Впрочем как и энумератор. Используйте русский перечеслитель.

На джаваскрипте до появления let все было еще хуже. А на C# время жизни переменной всегда определялось блоком, а не методом. Вот и ноют иногда :)

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

В C# итератором обычно называют реализацию IEnumerator с использованием yield return. Так что в контексте статьи не всякий перечислитель является итератором.

Реализация IEnumerator с использованием yield return всегда называлась "генератор", а не "итератор".

НЛО прилетело и опубликовало эту надпись здесь
С++ с вами не согласен :)

Это и правда фича. Потому что переменная цикла for — она принципиально общая для разных итераций и изменяемая!


Рассмотрим такой код:


for (int i=0; i<10; i++) {
  actions.Add(() => Console.WriteLine(i));
  if (i % 2 == 0) i++;
  actions.Add(() => Console.WriteLine(i));
}

Какое еще может быть тут поведение? Автоматически создавать новую переменную внутри цикла с тем же именем — нельзя, оператор i++ будет работать не так как ожидается.


Захватывать переменную по значению? Тоже нельзя, они же всегда захватываются по ссылке, за что такое исключение переменным цикла?


Даже создавать копию переменной только для захвата — тоже нельзя! Все из-за оператора i++ в теле цикла.


А уж если вспомнить, что внутри замыкания переменную цикла тоже можно менять — становится ясным, что цикл for может работать только так как он работает сейчас.


А вот с циклом foreach — ситуация другая. Там переменная цикла — неизменяемая, и никаких проблем в перетаскивании ее из внешнего блока во внутренний — нет.

А почему бы не дать возможность разработчику указать, как он хочет захватить — по ссылке или по значению? Так это сделано, например, в C++.
Так в статье 2 примера, в одном захват по ссылке, во втором — нет. Для этого не нужны специальные операторы.
Почему бы не дать возможность разработчику явно указать, как он хочет захватить? Явно и без привязки к циклам.

Например, так же, как это сделано для параметров функций — по умолчанию по значению, с ключевым словом ref — по ссылке.
Ну… " var index = " недвусмысленно указывает, что я хочу значение.

Вы хотите вместо этого явно где-то писать " ref int index "? Ну и выделите явно функцию, зачем вам замыкание?
В C# захват всегда происходит по ссылке, но во втором примере мы захватываем ссылку на копию объекта, сделанную внутри цикла, а в первом — на один объект для всех итераций цикла.
> ссылку на копию объекта,

А вы можете привести пример, где бы это имело значение (т.е. было бы важно)?
В статье есть 2 примера, которые наглядно демонстрируют отличия в поведении.
Не вижу смысла пересказывать то, что уже написано.
Тогда я не понял что именно вы хотели до меня донести
Потому что необходимость подумать и выбрать — это мука?
Очевидно потому, что в C# объекты передаются ТОЛЬКО по ссылке. Городить этот огород только для value типов смысла мало.

Нет, вы не поняли. Речь идет не о передаче объектов, а о захвате переменных. И в данном случае, захватываются по ссылке как объекты, так и value-типы! Посмотрите пример с циклом for: там индекс i вполне себе value-тип, но захвачен по ссылке.

Кстати, недопонял. В этом примере для каждой лямбды будет отдельный внутренний класс? Или обе в одном будут?

Почему компилятор использует пару инструкций stloc.3, ldloca.s, а не stloc.3, ldloc.3?

Потому что переменная номер 3 — это структура. А чтобы вызвать метод у структуры, нужен ее адрес.

Когда вышел C# 5.0, здесь уже была статья, что для цикла foreach поведением изменено таким образом, что на каждой итерации переменная для хранение текущего значения пересоздается (с возможностью через ключ компилятора вернуть старое поведение),
и что для цикла for остается старое поведение — как раз по причине того, что счетчик for принципиально един для всех итераций (по сути — сахар для while с объявленной вне тела цикла переменной-счетчиком).


Но за новую статью — спасибо. Более понятно и наглядно написано, как показалось.

Тоже ответил неверно. Java просто не позволяет изменяемые переменные захватывать и думаю что правильно делает. Хотя и не понимаю почему нельзя просто копировать значение (ссылку если объект).
Если копировать ссылку (можно переписать пример с reference type), будет такая же песня )) В контейнере на момент выполнения actions будет последний экземпляр. Вся суть в том, что используя замыкание, по факту, вы уже работаете не с оригинальной переменной, а с публичным полем класса-контейнера. Ну и как следствие — хранимое там значение регламентируется временем жизни экземпляра этого класса-контейнера.
Просто копировать значение нельзя, потому что пишут например такой код, который в этом случае сломается
var runInserts = true;
Task.Run(() =>
{
    while (runInserts)
        {
        }
});
...
runInserts = false;
НЛО прилетело и опубликовало эту надпись здесь
Статья однозначно полезная и интересная с технической точки зрения. Единственный спорный момент — утверждение, что реализации замыкания для for ошибочна и что это бага.

Выше уже неоднократно говорили о том, что различия в поведении замкнутых переменных для for и foreach вызваны различием в семантике обоих циклов.
Почему бага, статья при этом утверждает, что это фича. Да и собственно логичное поведение. Разве что управлять этим нельзя, ничего менять не надо. В крайнем случае сделать как в C++, только с текущим дефолтным поведением.
То, что вы считаете фичей, джависты считают ошибкой компиляции
Компилятор в C# честно предупреждает.
Всего лет на 10 задержались:) https://blogs.msdn.microsoft.com/ruericlippert/2009/11/12/1094/

На хабре тоже где-то столько же лет назад про это точно было уже :-).

Дочитал до конца, надеялся до последнего на чудо. Увы.
Имхо, точно написано — проблемы начинающих разработчиков. Каждый так или иначе стреляет себе в ногу, вот и вы попали :) Welcome to the club!
Только недавно наткнулся на что-то подобное. Захватил в лямбде поле класса и удивлялся, чего у меня неправильно работает. Поле в процессе работы переписывалось, а в вместе с ним менялось и то, что попало в замыкание.

Помог опыт Go, где есть точно такая же фича и цикл из примера будет работать точно так же.

Вообще-то, поле класса будет "замыкаться" одинаково вне зависимости от языка программирования! Разве что в Rust компилятор на что-нибудь ругнется...

Планируете ли Вы в будущем выпустить плагины для таких IDE, как CLion и Rider для более удобной и продуктивной работы с PVS-Studio в Linux системах? Спасибо.
Зарегистрируйтесь на Хабре , чтобы оставить комментарий