Комментарии 62
Интересно почему изначально была выбрана видимость per-loop, а не per-iteration? Для экономии памяти?
наверно, просто не продумали этот момент, сделали как проще
Традиция. Во многих языках есть (или было) такое же поведение: питон, ява, C#, javascript. По-отдельности реализация циклов и лямбд выглядит естественной, но их взаимодействие даёт неочевидное последствие.
ява
Будьте любезны, пример. Что-то придумать не могу, учитывая что в замыкание в Java можно захватить только final или effectively final переменную.
В java не так (там нельзя передавать не effectively final). В javascript не так (всё нормально захватывается). В python свободная переменная передаётся по имени, это вообще что-то из 60-х и алгола, но это понятное поведение.
c# проверить не могу (и не хочу), но думаю, там или как java или как javascript.
Дурость с for я видел только в Go. К сожалению, язык миновал стадию проектирования и сразу ушёл в продакшен.
В javascript не так (всё нормально захватывается).
Вот прямо сейчас запустил в браузере:
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value:", i);
};
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}
Результат:
My value: 3
My value: 3
My value: 3
c# проверить не могу (и не хочу), но думаю, там или как java или как javascript.
Было как в javascript, потом пофиксили (нет, не так как сделано в java, где запрещено захватывать переменную цикла). Тут в комментариях уже про это писали.
Вы в курсе что с 2014 года уже не используется var? Как минимум в хроме, остальные подтянулись к 2016. То есть минимум 7 лет как проблемы нет.
С let другое поведение?
Вы в курсе что с 2014 года уже не используется var
Цитирую себя: "Во многих языках есть (или было) такое же поведение"
В js, оказывается, и есть и было. Хорошо, буду знать, спасибо.
Просто добавлю, что let
был в фаерфоксе со времён первой браузерной. Но сначала его принципиально не хотел понимать ИЕ, потом хром. Потом разработчики хрома таки что-то поняли.
Ну, разработчики IE тоже поняли, только поздновато — в 11-й версии ;-) И то не во всех режимах.
Была у меня с IE такая вот хохма во дни минувшие. Делаю модуль расширения для ADFS на Win2012 R2. Ему там положено возвращать фрагменты HTML, которые ADFS вставляет в свой шаблон и возвращает получившуюся страницу пользователю. Проверяю работу в IE11- let в скриптах в фрагментах не работает. А те же самые фрагменты, вставленные в статический файл HTML — копию возвращаемой страницы — работают на ура. Сперва поофигевал, потом разобрался: AD FS передавал в IE заголовок, включающий режим совместимости с IE10 — а в том режиме let предусмотрен не был.
Хорошо когда клиенты обновляют свое ПО и компьютеры.
У меня 20% клиентов использует IE
В С++ никаких новых переменных на каждой итерации не создаётся. Но там захватывать переменную цикла по ссылке - уже взвод курка для выстрела в ногу.
В плюсах такой проблемы нет, из-за другой модели работы с памятью. Если мы захватим переменную по указателю то по завершении цикла мы получим dangling pointer. В го же из-за автоматического управления памятью так сделать можно без того чтобы получить ошибку доступа к памяти.
в питоне такое же поведение:
In [1]: func = [lambda: i for i in range(5)]
In [2]: func[0]()
Out[2]: 4
В питоне-то понятно, почему: тело функции (в данном случае лямбды) не интерпретируется до момента её вызова. Там любой лексически верный мусор можно написать между : и for.
>>> func = [lambda: j for i in range(5)]
>>> j = 'Hello'
>>> func[0]()
'Hello'
А что вы хотели показать этим примером? Это вполне ожидаемое поведение, если нет локальной переменной с таким именем, то питон будет искать среди глобальных переменных (вернее Enclosing, Global, Built-in). Ну и при этом на момент объявления функции эта переменная существовать не обязана.
In [1]: def f():
...: print(j)
...:
In [2]: j = "Hello"
In [3]: f()
Hello
Ну да, так всё и есть.
Я хотел показать именно то, что странно было бы ожидать, что значение i будет меняться в элементах списка func на каждой итерации, если оно фактически востребуется только один раз в момент вызова функции в последней строке.
Для такого поведения, как в новом Go, в питоне нужно было б заводить отдельный контекст для каждой итерации цикла, что обессмыслило бы само понятие перементой цикла.
c# проверить не могу (и не хочу), но думаю, там или как java или как javascript.
Там loop-level в for
, а вот в foreach
зависит от версии языка: изначально было loop-level, но в C# 5 поменяли на iteration-level.
c# проверить не могу (и не хочу)
Об этом было очень важно упомянуть.
В Python у циклов (как и у if) вообще нет своей области видимости, можно переменную вообще первый раз внутри цикла присвоить, и снаружи потом использовать. Если не присвоишь - просто будет NameError
>>> for i in range(10):
... a = 2
...
>>> print(i)
9
>>> print(a)
2
>>> print(c)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined
Вы еще спросите, почему в JS видимость this везде разная.
Просто провтыкали, как обычно.
Прощай старый добрый способ докопаться на собесах)
Я вот не понимаю, почему на собеседованиях спрашивают подобный кринж. Вот джаваскриптеров вообще любят спросить о том, что выведет лютая мешанина из разных скобок
На собесах хорошо отлавливает джунов
Если человек помнит или хотя бы подзабыл, перед вами джун, если прокричал что-то на орочьем, достал топор и сближается, значит про опыт не врет
По-моему такой вопрос реально отражает опыт, я сам с этим поведением for ... range cтолкнулся.
Меня больше смущают вопросы про поведение, если писать (или читать) в закрытый канал (буфферизованный), если так приходится делать - это уже плохой код. Зачем даже задуматься о том, какое будет поведение?
Минус один вопрос на собесах и в тестах! ха-ха(
В С# сделали то же самое в 2012 году - полет нормальный, никто не жалуется.
на самом деле мне интересно, как это будет сделано.
сейчас создается одна переменная с скоупом в весь цикл, и дальше в нее копируются данные на каждую итерацию.
а будет как? на каждую итерацию будет новая переменная?
звучит очень дорого, а если в цикле миллион итераций?
Звучит бесплатно, если речь о примитивах.
Ссылка на примитив как и любая другая ссылка весит 32 или 64 бита в зависимости от разрядности. Значение int весит 32 бита. В замыкание неизбежно что-то копировать да придётся - либо ссылку, либо значение. Копировать значение примитива стоит не дороже ссылки, а иногда дешевле (если у нас 64 битная ОС, а примитив 32 бита). Также чтение примитива по значению точно быстрее, чем по ссылке, потому что примитив читается за одну операцию, а примитив за ссылкой за две (сначала прочитать ссылку, потом значение по адресу из неё). Наконец, оптимизатор, зная что примитив никто за пределами лямбды не изменит, может лучше оптимизировать код.
В языках типа C++, где ссылки создаются более явно, давно есть правило, что примитивы по ссылке передают только если очень надо. По значению эффективнее. По ссылкам хорошие программисты передают объекты, которые занимают в памяти больше размера 2-3 указателей (никакие примитивы столько не весят), либо если нужны особые свойства ссылок (возможность менять из нескольких мест и т. д.)
Насколько понимаю, семантически захват в Go всегда по ссылке и просто скопировать значение в общем случае не получится. Скажем, два захвата на одной итерации должны получить одну общую переменную.
Новость об изменении этого поведения как раз. А мой комментарий о том, что для примитивов это повысит эффективность, а не понизит. Если примитивы в Go как в Java (выделяются не в куче и не имеют таблицы виртуальных методов), а не как в Python.
да не копируем итератор итератор в отдельную переменную на каждой итерации (и замыкаемся по ссылке на копию).
Если копией не воспользовались - всё отлично DCE её легко удалит.
Думаю, что если нет захвата переменной в замыкание, то можно и не создавать лишнего.
А как сейчас в Го продляется жизнь замкнутых переменных со стека?
Через двойной указатель и копирование в кучу при выходе из скоупа?
Ну и теоретически можно предусмотреть отдельную машинерию только если замыкание захватывает переменную цикла.
Upd: если в го есть понятие "объект расположенный на стеке" (с конструкторами \ деструкторами которые компилятор умеет элиминировать если они пустые) - то даже специальный случай вроде не потребуется.
Она не продляется, для этого есть escape analysis - если он говорит что значение переживает функцию оно сразу аллоцируется в куче
Спасибо.
Тогда моё update наверное не верно. Если у примитивных типов нет технического деструктора (вызываемого language-runtime, в абсолютном большинстве случаев не вызываемого), не на что навесить нужную логику (вводить его сейчас, понятно, поздновато).
И значит скорее всего надо отдельно делать циклы без замыканий (ничего не менять) \ отдельно циклы где замыкания захватывают переменную цикла.
Копируете переменную цикла. Замыкаетесь по копии переменной. Удаляете ненужные скопированные переменные (на стеке DCE справится, а вот если сделали выделение памяти - надо уже ручками удалять).
ПС
Желательно эти 3 фазы поставить подряд.
Если анализ гитхаба и другого опенсорса говорит, что ничего не сломается от такого изменения, то ничего страшного.
Ломающее изменение и подъем только минорной версии языка? Что-то я не понимаю в semver
"Если раньше в циклах были проблемы с замыканиями, так как переменная цикла имела скоуп".
... Им это слово много говорило. Жаргон это конечно хорошо, но все же...
Даже ПЕРВЫЙ ЖЕ коммент использут нормальное слово "видимость".
Тут в соседней статье пишут "холодный аутрич"
скоуп уже давно общепринятый термин. Ну и кстати...
жаргон - это французское слово
коммент - это английское слово
Scope часто переводят как область видимости. Но ключевое слово — область. "Видимость" в первом комментарии в контексте статьи не вызывает вопросов, но в другом контексте может вызывать.
Если бы можно было всем договорится о "правильном" переводе, я бы выбрал "ареал".
какой же у го отвратительный синтакс
О, я с таким сталкивался, совершенно не ожидая такого поведения. Подумал "фу" и обернул тело цикла в вызов функции, совсем как в js когда-то
В Go меняется фундаментальная вещь — цикл