Где найти старый компилятор для C# 4.0
где найти старый компилятор для C# 4.0 желательно онлайн. Просто интересно для проверки closure. В старом по идее не должно быть автовключения строки 13
https://paiza.io/projects/e/MSCBn5zG7GL-FfyOJquZMg?theme=t...
Все что перепробовал добавляют и получается "правильный" вывод.
здесь
https://habr.com/en/post/141270/
вроде понятно описано, но в IL не вижу этого. Тоже хочу разобраться.
ну и присваивание немного другое должно быть. Я просто мои тесты не убрал полностью что бы идея был понятна.
до 2010 включительно
https://www.tutorialsteacher.com/csharp/csharp-version-his...
можешь код компильнуть что по ссылке на 2005 или 2010, что будет в результате? Если 10,10,... то интересно экзешник поиметь и IL глянуть. А то новые декомпиляторы всю картину "портят"
Только это совершенно неважно, если есть какие то проблемы то не нужно.
Вроде кажется дошло, поправьте если неправильно.
Для использования локальных переменных в лямбде/анонимной функции делается как бы снимок всех требуемых переменных в специальном closure class который решили назвать DisplayClass
https://stackoverflow.com/questions/16401860/what-does-dis...
Если внутрь цикла вставить ненужный оператор присваивания, то компилятор создает closure class каждый раз внутри цикла и делает снимок. Без этого оператора closure class создается вне цикла и также делается снимок внутри цикла. То бишь имеем в одном случае 1 экземпляр с 10 присваиваниями в цикле , а в другом случае 10 экземпляров с одним присваиванием. closure class создается в месте "минимальной видимости" переменных.
for (int i = 0; i < 10; i++) { int tmp = i; Action action = delegate() { Console.WriteLine(tmp); }; actions.Add(action); }
Не надо его нигде искать. Он на компе есть уже - у каждого.
Открываем cmd.exe и пишем:
C:\Windows\Microsoft.NET\Framework\v3.5\csc /out:C:\v4.exe с:\path_to\Program.cs
А потом пишем:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc /out:C:\v5.exe с:\path_to\Program.cs
Затем открываем любым декомпилятором v4.exe и v5.exe и сравниваем - хоть C#, хоть IL.
Да иногда думать *нихами полезно. Спасибки
Действительно, не обманули, для 4-ки foreach работает "неправильно".
Зато какие интересные побочные эффекты можно получить
var actions = new List<Action>(); int j = 0; int i = 0; for (; i < 10; i++) { //int tmp = i; Action action = delegate() { Console.WriteLine(String.Format("{0} - {1}",i, j++)); }; actions.Add(action); } int k = 0; foreach (var action in actions) { if (k % 2 == 0) { action(); } k++; }
10 - 0
10 - 1
10 - 2
10 - 3
10 - 4
Эт феерия какая-то. Т.е. они делали closure (замыкание) по значениям, не ссылкам, но "соптимизировали" его, типа а чо, не меняется же ничего, переменные всё те же, можно новый не создавать. Здорово, память сэкономили, но блин, ведь значения поменялись, а closure у них по значениям, а?
Я так понял что в 5-м микрософт поправил поведение в циклах. А что он сейчас скомпилирует если написать (без цикла)
int i = 1; actions.Add(() => Console.WriteLine(i)); i = 2; actions.Add(() => Console.WriteLine(i));
Опять один closure (по значениям!) в котором будет только последняя 2-ка висеть и при отработке Action-ов выдаст нам "2 2"?
Замыкание в C# делается всегда на переменную, а не на значение.
Прикол был только в версии C# до 4.0 включительно и только с циклом foreach. Связано это с тем, как цикл foreach был специфицирован - во что он развёртывался. Там при развёртывании переменная цикла foreach была задекланирована вовне цикла while. С замыканиями это вообще никак не связано было, просто это проявилось после введения замыканий. В 5 версии изменили спецификацию foreach, а не замыканий.
Не нужно устраивать истерику, не разбираясь в материи.
Да результат 2 2
https://paiza.io/projects/62VDITV6xh1IxfbdmnGF2g?language=...
Нефиг подобные лямбды как action пользовать
Давайте всё же как-то общепонятную терминологию использовать. "делается всегда на переменную, а не на значение." это что? Вы хотите сказать что в шарпе closure (не люблю термин "замыкание") хранит не значения, а ссылки на "захваченые" переменные (free variables, определенные снаружи функции), так?
Я хочу сказать, что в C# замыкание делается на переменную, а не на значение этой переменной в момент создания замыкания.
Так понятнее?
Техническая реализация (ссылка, не ссылка) - это другой вопрос. Для ссылочных типов там, понятно, ссылка хранится. Для значимых типов - значение.
Вот этот пример как раз отлично это иллюстрирует:
int i = 1; actions.Add(() => Console.WriteLine(i)); i = 2; actions.Add(() => Console.WriteLine(i)); // 2 // 2
даже не стрелять - вешать нужно на площадях тех, кто подобной херней балуется, думая, что он при этом умнеет. он болванеет, этого не замечая.
Не согласен. Детальное понимание орудия труда (фич языка программирования) приводит в порядок ум, позволяет решать задачи эффективно и поднимает человека с уровня джуниор-миддл до уровня сениор.
Да, в ставке мелкомягких все малохольные.
var actions = new List<Action>(); int i = 1; actions.Add(() => Console.WriteLine(i)); i = 2; actions.Add(() => Console.WriteLine(i)); var actions2 = new List<Action>(); foreach(int j in Enumerable.Range(1,2)){ actions2.Add(() => Console.WriteLine(j)); } Console.WriteLine("1:"); foreach (var action in actions){ action();} Console.WriteLine("2:"); foreach (var action in actions2) { action();}
Радостно выдаёт (поправил, надо было копировать вывод а не перенабирать :), цикл выдаст "1 2", два вызова Add с изменением в промежутке i - "2 2")
1: 2 2 2: 1 2
От это я понимаю, исправили поведение foreach, теперь всё стало ясно и понятно. Уж лучше как в яве, запретить использовать переменные из scope, только константы (final).
Я хочу сказать, что в C# замыкание делается на переменную, а не на значение этой переменной в момент создания замыкания.
Так понятнее?
вроде бы стало понятнее, т.е. "замыкание со ссылками", т.е.
int i = 1; actions.Add(() => Console.WriteLine(i)); i = 2; // печатаем
выдаст нам не 1 а 2 (хотя не ссответствует коду Closure из статьи на хабре).
Но тут вы меня опять смутили:
Для ссылочных типов там, понятно, ссылка хранится. Для значимых типов - значение.
Это как? Ссылка, хранимая в переменной "x" ссылочного типа и есть её значение. "замыкание со ссылками" будет хранить ссылку на переменную "x" а не ссылку, хранившуюся в "x" в момент создания closure. Получается в моём примере выше, шарп должен выдать 1, ведь при создании closure он соханил в нём для значимого типа его значение "1"?
Вот этот пример как раз отлично это иллюстрирует:
Не совсем отлично. Тут или шарп создал одно-единственное closure для обеих лямбд (вторая экшен это та же первая), и при инициализации для 2-го action переписал значение i (замыкание со значениями), или он создал два closure но со ссылками на i (замыкание со ссылками).
Тут или шарп...
В результате эксперимента+ILSPY получилось следующее
- Closure создается для каждой функции.
- Action создается для каждой лямбды
- переменные создаются которые нужны
- перед созданием делегата вызывается Closure.j = 20; - это я просто добавил для теста
IL_0042: ldc.i4.s 20 // 0x14
IL_0044: stfld int32 ClosureTest.Program/'<>c__DisplayClass0_0'::j
Не хочу азы объяснять. Если интересно - читайте разницу между ссылочными типами и значимыми в C#.
На абстрактные философствования отвечать не буду.
foreach исправили отлично, кто знает язык, тот будет понимать. В первом случае переменная i одна и та же для обоих замыканий, поэтому "2 2" логично. Во тором случае, переменная хоть и называется одинаково, но в каждой итерации цикла она разная, поэтому "1 2". Всё прекрасно и логично.
Кто не согласен - можно идти лесом, а не выпендриваться про "мелкомягких". C# - один из самых красивых и логичных мультипарадигменных языков на сегодняшний день. В отличие от всяких жаб и плюсов.
Не хочу азы объяснять.
Не хочу или не могу? Есть небольшая разница.
Кто не согласен - можно идти лесом, а не выпендриваться про "мелкомягких".
Какая прелесть. Раз вы тут советы раздавать взялись, я вам тоже посоветую. Прежде чем высказывать сверхважное мнение следует немного разобраться в теме. Прочитать про замыкания и 2 (всего навсего) теоретические возможности захватывать переменные окружения, обычно не сложно. Если бы вы это сделали, то поняли бы в чем разница захватывать ссылки на переменные и значения переменных, а не пели бы про разницу ссылочных и значимых типов в шарпе (стек или куча - неважно). А там, глядишь, и самоуверенности с понтами поубавится.
В общем, получается что в шарпе closure со ссылками. Странное Отважное решение, я, честно говоря, не помню в каком еще языке такое есть. Оно было бы логично если бы и методам параметры по умолчанию по ссылке а не по значению передавались бы. А так в шарпе есть ref (привет функциональному программированию, блин), чего бы им при определении лямбд подобным не пользоваться? Хочешь по значению (без ref), хочешь по ссылке.
Проблема с захватом ссылок а не значений в том, что использование closure и присваивание значений переменным может быть сильно разнесено. Забудешь что у тебя closure была, наприсваиваешь чего переменным и всё, 100 строчками кода позже кто-то вызовет такую сохранённую функцию и порадуется непонятному результату. Удачи в дебаге!
У меня другой
вопрос - получается что сейчас поведение лямбд, определённых внутри for и foreach будут отличаться, for может работать с ранее определенной переменной, в foreach надо определять свою переменную цикла.
Можете посмотреть поведение 4-й версии с foreach? Сейчас в foreach нельзя присваивать переменной цикла других значений. (просто написать i = 5; внутри). А до 5-й версии (пока foreach не "исправили") тоже нельзя было, или можно?
В общем, получается что в шарпе closure со ссылками.
Что то я вас не пониманию. Где тут ссылки? closure.i = i; Для меня это как бы indirect usage
https://paiza.io/projects/62VDITV6xh1IxfbdmnGF2g?language=csharp
for может работать с ранее определенной переменной, в foreach надо определять свою переменную цикла.
как раз наоборот. Изменили поведение foreach. Closure теперь определяется не снаружи цикла, а внутри.
Сейчас в foreach нельзя присваивать переменной цикла других значений.
Сорри, это тоже что то не понимаю. Ни раньше ни сейчас в foreach нельзя самому менять переменную цикла. Иначе сам принцип изменится.
Тут вроде еще нормально описано
https://itvdn.com/ru/blog/article/closing-in-csharp
Это же надо еще до подобного использования додуматься.
Что то я вас не пониманию. Где тут ссылки? closure.i = i; Для меня это как бы indirect usage https://paiza.io/projects/62VDITV6xh1IxfbdmnGF2g?language=csharp
Хм. Где в вашем коде использование closure? То что вы свой класс так назвали ничего не значит. Вернитесь к примеру с лямбдами.
как раз наоборот. Изменили поведение foreach. Closure теперь определяется не снаружи цикла, а внутри.
Я имел в виду следующее:
int i = 10; for(;i<20;i++){...} // можно int j = 0; foreach(j in ...) {...} // нельзя // ^ - должна быть новая переменная
Сорри, это тоже что то не понимаю. Ни раньше ни сейчас в foreach нельзя самому менять переменную цикла. Иначе сам принцип изменится.
Почему это? в начале каждой итерации переменной присваивается значение из списка. Но что мешает мне в блоке foreach менять значение этой переменной? Код на яве: https://paiza.io/projects/e/W6I0zp-bwEBXCWdBekivdA
int[] values = new int[]{1,2,4,8}; for(int i : values){ i = i+1; // в шарпе нельзя System.out.println(i); }
Выдаст 2,3,5,9
Повторяю - в C# замыкания всегда происходят на переменную, а не на значение переменной в момент создания замыкания. Что тут непонятного? Если реально не понятно, то вперёд читать букварь. Спецификацию языка то есть. Техническая реализация в CLR остаётся за скобками, поэтому "ссылка/не ссылка" - вообще никакого значения тут не имеет.
Повыпендриваться "не хочу или не могу" советую перед джуниорами на работе, мне это слушать не интересно.
где в вашем коде использование closure?
Вот
var closure = new Closure();
....
closure.i = i;
var action = new Action(closure.Action);
actions.Add(action);
Можно было оставить DisplayClass, но это тоже ничего не меняет ведь компайлер "разворачивает" лямбду в подобный код.
Понятно, что вы смотрите на это с какой то другой стороны. Но пока не могу догнать с какой.
Я имел в виду следующее:
так вроде никак не получится.
15.8.4 The foreach statement
The foreach statement enumerates the elements of a collection, executing an embedded statement for each
element of the collection.
foreach-statement:
foreach ( type identifier in expression ) embedded-statement
я думал что так
foreach(int j in ...) {j=0; ...}
В С# 7.3 вроде поведение как то изменили, но еще не игрался
Почему это?
Отчего в Яве так не знаю. В шарпе foreach коппайлер "переписывает". Да и стандарт не позволяет The iteration variable corresponds to a read-only local variable https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web...
Вот если тут коммент убрать так даже компилится не будет
https://paiza.io/projects/tEMtl6riuHO1bb1LK97ejQ?language=...
Вот
var closure = new Closure();
тут вы просто создаёте объект вашего класса Closure. К замыканиям отношения не имеющим. Замыкание создаётся компилятором когда вы, например, определяете лямбду () => expression.
ведь компайлер "разворачивает" лямбду в подобный код.
Кто сказал? Автор статьи на хабре? Он не прав. Проверка:
int i = 5; actions.Add(() => Console.WriteLine(i)); // вот тут возникнет closure содержащее эту лямбду и ссылку на i i = 50; actions[0]();
Как по-вашему, что выдаст?
Вот если тут коммент убрать так даже компилится не будет
https://paiza.io/projects/tEMtl6riuHO1bb1LK97ejQ?language=...
Вот это мне и интересно, это так с самого начала было или с 5-й версии, а до нее не было "read-only local variable"? Просто интересно было как именно они поведение foreach поменяли.е
Кто сказал?
Декомпилятор, выложить IL? https://github.com/icsharpcode/ILSpy/releases
Только нужно сказать чтобы все показывать
Как по-вашему, что выдаст?
А не нужно гадать, можно просто глянуть
Просто интересно было как именно они поведение foreach поменяли
Достаточно просто, раньше это было вне цикла
var closure = new Closure();
А потом внутри
Копаться в коде, сгенерированном компилятором, конечно, очень интересно. Но не стоит объяснять реализацию конкретной фичи языка с помощью этого сгенерированного кода - он сильно зависит от версии компилятора и может запутать. Как раз это и происходит сейчас.
То есть, я бы сказал так. Шаг первый. Объясните человеку фичу языка "на пальцах". Логически - что происходит, какие условия должны выполняться и т.д. Когда фича понята, можно приступать к шагу второму: как это реализовано в "компиляторе версии такой-то". Шаг второй нужен, если есть желание досконально разобраться в материале или нужно писать high-perf код и требуется чёткое понимание, что там компилятор такого нагенерирует.
А то получается, объясняем async/await с помощью сгенерированных IAsyncStateMachine структур - тут 90% гуру и сеньоров будут долго репу чесать, чтобы понять всю эту магию. Не нужно так делать.
Изменение foreach элементарное, но сильно влияет на замыкания.
До 4.0 включительно, foreach разворачивался так:
foreach (V v in x) embedded-statement --> { E e = ((C)(x)).GetEnumerator(); try { V v; while (e.MoveNext()) { v = (V)(T)e.Current; embedded-statement } } finally { … // Dispose e } }
Начиная с 5.0, foreach разворачивается так:
foreach (V v in x) embedded-statement --> { E e = ((C)(x)).GetEnumerator(); try { while (e.MoveNext()) { V v = (V)(T)e.Current; embedded-statement } } finally { … // Dispose e } }
Единственная разница - область видимости переменной итерации v. Именно это и влияет на то, что происходит с замыканиями. В первом случае замыкание будет на одну переменную для всех итераций, во втором - множество замыканий на разные переменные в каждой итерации.
При этом сама переменная итерации v была и остаётся read-only во всех версиях языка. Это по спецификации, компилятор это гарантирует. Из кода выше этого не видно, потому что это реализовано не кодом, а компилятором. Если убрать эту проверку из компилятора, то технически переменную итерации можно изменять, код это позволяет.
Но не стоит объяснять реализацию конкретной фичи языка с помощью этого сгенерированного кода - он сильно зависит от версии компилятора и может запутать
безусловно, что зависит, но основной принцип, по идее остается неизменным.
Переменные контекста, все равно нужно как то передать. Переменных может быть много, значит нужен контейнер. При этом есть два шага: создание контейнера и копирование контекста.
Эти два шага и могут отличаться от версии к версии для конкретных применений. Это вы имели в виду под логикой или что то другое? Но вполне возможно, что я еще не все до конца понял.
А вот что код может запутать, это видимо, от человека зависит. Мне лично как раз удобнее код глянуть,чем многостраничные объяснения читать.
А то получается, объясняем async/await
Наверное нужно тоже тему сделать, чтобы точно до конца все добить. На Ютубе только одно приличное видео нашел пока, но все равно не думаю, что на абсолютно все вопросы смогу сразу ответить.
Это нужно пару лет их пользовать чтобы в "кровь вошло".
В локальных функциях замыкания технически реализованы по-другому. Но по спецификации - это те же самые замыкания, что и в лямбдах. Именно это я имел в виду. Для понимания концепции необязательно лезть в дебри сгенерированного кода и изучать внутренности CLR.
В локальных функциях
ааа... Я их как-то принципиально игнорирую. Пока во всяком случае. Хорошо это или плохо не-знаю.
Надо будет код глянуть...
Для понимания концепции необязательно лезть в ...
А какой есть еще способ - искать приличное описание?
В локальных функциях замыкания технически реализованы по-другому
А где есть описание, потому как у меня так получилось.
https://paiza.io/projects/OEtazpJ6NB7St4wW7HxA5Q?language=...
Большой разницы не вижу.
Да, просто для локальной функции получается по другому.
https://paiza.io/projects/IBYy3C_aqEty-9GayHOupg?language=...