Правильно ли я понимаю, что тип переменной хранится в рантайме рядом с самой переменной, и используется для выбора одной из функций из нескольких одинаковых по назначению, отличающихся только типами (размерами) аргументов?
Ещё мне непонятно, зачем нужно ключевое слово var в C#. Да, исторические причины, привычки, то-сё.
Но если проектировать синтаксис языка начисто, то:
во-первых, тип переменных заранее объявлять не нужно (он выводится);
во-вторых, тип переменной можно сделать зависимым от контекста,
для того чтобы можно было в двух последовательных строчках написать:
data=1; // целое
data="lala"; // строка
Язык при этом останется типизированным, потому что
компилятор будут генерировать разный код в зависимости от типа переменной в месте использования.
Рантайм не будет хранить тип переменной, потому что об этом будет заботится компилятор.
питон широко использует динамическое связывание переменных, которое никуда не девается в откомпилированном коде
ЭФ>https://habr.com/ru/post/481782/
ЭФ>Правильно ли я понимаю, что тип переменной хранится в рантайме рядом с самой переменной, и используется для выбора одной из функций из нескольких одинаковых по назначению, отличающихся только типами (размерами) аргументов?
Нет. Связывание — это процесс, который связывает имя объекта с самим объектом.
Чаще всего этот термин употребляют в контексте поиска метода — к примеру, Function(x) может ссылаться (в зависимости от языка и контекста) на десяток разных методов с именем Function.
Если мы говорим про переменные, то каждая переменная — это, в конце концов, некая "ячейка памяти", которая однозначно определяется её адресом.
Соответственно, связывание переменных — это сопоставление имени некоторого адреса.
Практически в любом языке программирования одно и то же имя может сопоставляться с разными адресами, даже в один и тот же момент времени:
int main() {
static int x = 5;
std::cout << x << endl; // это - x №1
{
static int x = 6;
std::cout << x << endl; // это - x №2
}
std::cout << x << endl; // а это - снова x №1
}
public int CountTreeLeaves<T>(T node)
where T: IEnumerable<T>
{
var c = 0;
foreach(var child in node)
c+= CountTreeLeaves(child); // когда мы обходим глубокое дерево, у нас одновременно существуют несколько экземпляров переменной с именем "c" - по одной на уровень вложенности вызоваreturn c;
}
Но в нормальных, некурящих языках, это сопоставление делается статически — то есть при компиляции программы. В первом примере у нас получилось два разных x с двумя разными адресами. Но вопрос о том, куда обращаться при выводе x, решается один раз, при компиляции, а при исполнении уже нет никакой свободы выбора. Иногда, как в первом примере, связывание получается абсолютным — т.е. адрес ячейки однозначно известен; даже если мы вызовем main() рекурсивно, оба x будут ссылаться на те же адреса, что и во внешнем вызове. Иногда, как во втором примере (и вообще в большинстве случаев) адреса указываются относительно текущего фрейма стека (что и позволяет с иметь одновременно множество не мешающих друг другу значений).
Те места, где связывание делается динамически, в таких языках обозначаются специальным образом (и, как правило, это относится исключительно к связыванию методов, а не переменных).
А в языках типа JS и питона связывание переменных выполняется динамически. То есть адрес ячейки памяти, к которой нужно обратиться при вычислении выражения с участием переменной, определяется в момент исполнения программы; заранее этого сделать нельзя из-за правил связывания, описанных в языке.
ЭФ>Ещё мне непонятно, зачем нужно ключевое слово var в C#. Да, исторические причины, привычки, то-сё. ЭФ>Но если проектировать синтаксис языка начисто, то: ЭФ>во-первых, тип переменных заранее объявлять не нужно (он выводится); ЭФ>во-вторых, тип переменной можно сделать зависимым от контекста, ЭФ>для того чтобы можно было в двух последовательных строчках написать: ЭФ>data=1; // целое ЭФ>data="lala"; // строка
И какова будет семантика этого обращения?
Что должно быть выведено, если третьей строкой мы напишем System.Console.WriteLine(data)?
ЭФ>Язык при этом останется типизированным, потому что ЭФ>компилятор будут генерировать разный код в зависимости от типа переменной в месте использования.
Это как раз понятно. Непонятно, как определить тип переменной "в месте использования". ЭФ>Рантайм не будет хранить тип переменной, потому что об этом будет заботится компилятор.
Это и есть статическое связывание, которое уже и так используется в C#. ЭФ>Что со мной не так?
Нехватка образования. Ничего страшного — это поправимо
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
S> заранее этого сделать нельзя из-за правил связывания, описанных в языке. S> Нехватка образования.
А что у питона не так с правилами (какие именно правила есть в питоне)?
Почему в питоне заранее этого сделать нельзя?
Помог бы пример. Но на самом деле мне это неинтересно.
S> Непонятно, как определить тип переменной "в месте использования".
В соответствии с ДРУГИМИ, разумными правилами языка.
S> какова будет семантика этого обращения?
Непонятно, почему это важно. Ну пусть определяется временем и скопом.
Т.е. в одном блоке выигрывает последнее присвоение,
в разных блоках — по другим правилам.
Т.е. в данном случае напечатает "lala", а значение 1 будет потеряно (освобождено).
Здравствуйте, Эйнсток Файр, Вы писали:
ЭФ>Помог бы пример. Но на самом деле мне это неинтересно.
S>> какова будет семантика этого обращения?
ЭФ>Непонятно, почему это важно.
Потому, что это и есть семантика языка. Мелкие решения вроде этих приведут к достаточно существенным последствиям в удобстве использования языка и отладки программ на нём. ЭФ>Т.е. в одном блоке выигрывает последнее присвоение, ЭФ>в разных блоках — по другим правилам. ЭФ>Т.е. в данном случае напечатает "lala", а значение 1 будет потеряно (освобождено).
Ну, ок. Давайте так:
S>Что выведет программма? Какого типа будет data в строке 4?
ЭФ>>> Правильно ли я понимаю, что тип переменной хранится в рантайме рядом с самой переменной, и используется для выбора одной из функций из нескольких одинаковых по назначению, отличающихся только типами (размерами) аргументов? S>>Нет.
Здравствуйте, Эйнсток Файр, Вы писали:
ЭФ>Правильно ли я понимаю, что тип переменной хранится в рантайме рядом с самой переменной, и используется для выбора одной из функций из нескольких одинаковых по назначению, отличающихся только типами (размерами) аргументов?
Чтобы объявление новой переменной глазами отличалось от присваивания значения старой.
ЭФ>Но если проектировать синтаксис языка начисто, то: ЭФ>во-первых, тип переменных заранее объявлять не нужно (он выводится);
Выводится, если ты всегда совмещаешь объявление переменной с инициализацией. Но иногда хочется переменную объявить, а что в нее с самого начала положить, особых идей нет.
В Go:
var i int// Объявление переменной без инициализации
j := 5 // Объявление с автовыводом типа и иницализацией
k = 5 // Присваивание нового значения уже имеющейся переменной
ЭФ>Язык при этом останется типизированным, потому что ЭФ>компилятор будут генерировать разный код в зависимости от типа переменной в месте использования.
Статическая типизация в первую очередь нужна не для того, чтобы компилятор знал, какую функцию вызывать, а чтобы человек не перепутал, и не положил количество яблок в переменную, тип которой объявленн, как количество апельсинов.
А явное объявление переменных нужно, чтобы если компилятор видит x = 5, а про x ничего раньше не слышал, чтобы он не гадал, новая ли это переменная, или просто я ошибся в названии старой, а сразу мое внимание обратил на такую странность в коде, выдав соответствующую ошибку.
Это спасает от очень большого количества глупых ошибок.
S>>Что выведет программма? Какого типа будет data в строке 4? ЭФ>>>> Правильно ли я понимаю, что тип переменной хранится в рантайме рядом с самой переменной, и используется для выбора одной из функций из нескольких одинаковых по назначению, отличающихся только типами (размерами) аргументов? S>>>Нет. ЭФ>А как тогда питон это делает?
Динамически(во время выполнения программы), т.е. в зависимости от того по какой ветки исполнения пошли, такой тип и будет у data.
Здравствуйте, Эйнсток Файр, Вы писали: ЭФ>>>> Правильно ли я понимаю, что тип переменной хранится в рантайме рядом с самой переменной, и используется для выбора одной из функций из нескольких одинаковых по назначению, отличающихся только типами (размерами) аргументов? S>>>Нет. ЭФ>А как тогда питон это делает?
Во-первых, есть много разных питонов. Теоретически, ничто не мешает питону хранить разные типы в разных местах памяти, и определять тип, сравнивая значение указателя с известными диапазонами.
Во-вторых, выбор делается не обязательно из функций, одинаковых по назначению.
Например, для операции a + b проверяется, уж не являются ли обе переменных строками — тогда срабатывает конкатенация; иначе сложение.
Считать назначение конкатенации и сложения одинаковым можно только с натяжкой.
Ещё более отвязный пример — операция %: для строк она означает форматирование, которое никак не похоже по семантике на вычисление остатка.
Но это всё относится исключительно к типизации — она делается уже после того, как выполнено связывание переменной.
Смотрите, в Java, С#, JS обращение вида a+=1; может относиться к локальной переменной, к параметру метода, к полю текущего объекта.
В C# — ещё и к свойству текущего объекта, и к статическому свойству одного из классов, которые втащены в текущую область видимости при помоши using static.
Так вот, в Java и C# выбор между всеми этими вариантами определяется статически; в каждой строчке кода "a" имеет ровно одну, чётко определённую семантику. И она не зависит от того, какой код исполнялся перед тем, как исполняется эта строчка.
Если я правильно понимаю Питон, то там — не так. И смысл "а" в выражении a = a + 1 может меняться в зависимости от того, какой код исполнялся до этого.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Я не думаю, что в пайтоне динамическое связывание переменных. Я его видел только в скриптах и в Emacs Lisp-е. В подавляющем большинстве языков программирования статическое связывание переменных.
Пример динамического связывания переменных:
function f1() {
x = 1;
}
function f2() {
var x;
f1();
print(x); // 1
}
По сути при динамическом связывании переменных у тебя все переменные глобальные. Рекурсию устраивать вообще сложно. Суть статического связывания в том, что глядя на программу (до выполнения) и на одинаковый идентификатор в двух местах ты всегда можешь сказать — является ли этот идентификатор обозначением одной и той же переменной, или нет. В Python или JavaScript статическое связывание.
Я думаю, имеется в виду динамическая типизация (против статической). При динамической типизации у переменной может быть значение любого типа. При статической — только одного типа.
Здравствуйте, vaa, Вы писали:
vaa>Здравствуйте, Sinclair, Вы писали:
S>>Что выведет программма? Какого типа будет data в строке 4? vaa>F# shadowing vaa>
vaa>let test () =
vaa> let data = 1
vaa> let printData () = printfn "%A" data
vaa> printData ()
vaa> let data = "lala"
vaa> printData ()
vaa>
vaa>
>> FSI_0007.T1.test();;
vaa>1
vaa>1
vaa>val it: unit = ()
vaa>
vaa>локальная функция захватила первое значение.
Это ответ на какой-то другой вопрос. В вашем примере нет ветвления, поэтому можно считать, что data биндится к целому числу в строчках с 2 по 4, а в строчках 5 и 6 она биндится к строке.
То, что printData захватила первое значение, связано только с тем, что она расположена в тех строчках, где data — это 1.
Если мы заменим вызовы printData на printfn, то они выведут разные значения.
Тем не менее, в каждой точке у вас data имеет ровно один биндинг и ровно один тип.
А если мы перенесём let data = "data" внутрь условного оператора, то после выхода из блока shadow исчезнет, и data снова станет равным 1.
Это ненамного более интересно, чем shadowing в C++, пример которого я привёл в начале дискуссии.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Pzz> чтобы человек не перепутал, и не положил количество яблок в переменную, тип которой объявленн, как количество апельсинов.
А что по этому поводу думали авторы Rust, которые сделали кадую строчку переобъявлением переменной,
кроме тех переменных, которые помечены словом mut ?
// Rust C/C++
a: &T == const T* const a; // can't mutate either
mut a: &T == const T* a; // can't mutate what is pointed to
a: &mut T == T* const a; // can't mutate pointer
mut a: &mut T == T* a; // can mutate both
Здравствуйте, Эйнсток Файр, Вы писали:
Pzz>> чтобы человек не перепутал, и не положил количество яблок в переменную, тип которой объявленн, как количество апельсинов.
ЭФ>А что по этому поводу думали авторы Rust, которые сделали кадую строчку переобъявлением переменной, ЭФ>кроме тех переменных, которые помечены словом mut ?
Авторы rust думали о том, что 80% переменных в программе естественным образом присваиваются один раз и используются, по сути дела, как в математике, чтобы упростить чтение текста, дав короткое и простое название длинному и сложному выражению.
А и в тех случаях, когда переменная используется именно как переменная (именованная сущность, неоднократно меняющая значение), можно ее явно соответствующим образом объявить.
В общем, тоже для автоматического отлова типичных ошибок.
ЭФ>Можно было сделать shadowing по-умолчанию и сэкономить на словах let и var.
Тогда можно было бы случайно зашадовить переменную, и долго удивляться неожиданному эффекту.
P.S. Подкину для тебя новую мысль для обдумывания. В программе без циклов мутабельные переменные не нужны.
S>Что выведет программма? Какого типа будет data в строке 4?
dynamic Но не думаю, что компилятор должен такой тип выводить. Без объявления переменных далеко не уедешь. В том же питоне из-за отсутствия объявлений, появляются костыли вроде nonlocal.
Здравствуйте, Sinclair, Вы писали:
S>Но это всё относится исключительно к типизации — она делается уже после того, как выполнено связывание переменной. S>Смотрите, в Java, С#, JS обращение вида a+=1; может относиться к локальной переменной, к параметру метода, к полю текущего объекта. S>В C# — ещё и к свойству текущего объекта, и к статическому свойству одного из классов, которые втащены в текущую область видимости при помоши using static.
Не думал, что когда-нибудь такое скажу, но здесь примером может быть PHP. При обращении к полям $this или static обязательно.
S>>Что выведет программма? Какого типа будет data в строке 4?
A>dynamic
Ну, если идти по этому пути, то data будет иметь тип object. Но есть ли в этом смысл?
Что лучше — вывести вот так наиболее общий тип, выдать ошибку присваивания, или выполнить shadowing внутри then-блока оператора if?
По мне, первый и третий варианты плохи.
В первом у нас есть риск одним неосторожным присваиванием сломать тип переменной, и полностью поменять биндинги вызовов по всему методу. Это может иметь далеко идущие последствия — то есть семантика программы мгновенно поменялась (даже если у нас условие под if никогда не стреляет), и разработчик будет ломать голову о причинах такого поведения. Хорошо ещё, если у него есть IDE, где можно навести курсор на data и увидеть выведенный тип.
После этого всё ещё потребуется найти, какое же из присваиваний изменило тип, но хотя бы будет понятно, что не так. Без IDE надо будет ещё понять, что привело к срабатыванию другой перегрузки того же метода. Нафиг-нафиг.
Что здесь произойдёт — shadowing или присваивание? Тип на этот раз мы не меняем; если мы по-прежнему выполняем shadowing, то это жутко континтуитивно. Ещё хуже, чем K&R C, оставляющий без диагностики перлы вроде if(nextRandom==0); data = 2;
Если мы выполняем присваивание — то опять не легче. Только что у нас была инициализация строкой, которая выполняла shadowing, и не портила data ниже по коду; а теперь вдруг точно такое же по форме выражение стало присваиванием. В F# есть чёткое различие между let data = 2 и data = 2 — и из этих примеров видно, почему.
Во flow9 разрушающее присваивание разрешено только для специальных byRef типов, и его синтаксис отличается от инициализации — по той же причине. A>Но не думаю, что компилятор должен такой тип выводить. Без объявления переменных далеко не уедешь. В том же питоне из-за отсутствия объявлений, появляются костыли вроде nonlocal.
Дело даже не в объявлении переменных, а в синтаксической различимости конструкций "связать имя с выражением" и "выполнить разрушающее присваивание"
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, amironov79, Вы писали:
A>Здравствуйте, Sinclair, Вы писали:
S>>Но это всё относится исключительно к типизации — она делается уже после того, как выполнено связывание переменной. S>>Смотрите, в Java, С#, JS обращение вида a+=1; может относиться к локальной переменной, к параметру метода, к полю текущего объекта. S>>В C# — ещё и к свойству текущего объекта, и к статическому свойству одного из классов, которые втащены в текущую область видимости при помоши using static.
A>Не думал, что когда-нибудь такое скажу, но здесь примером может быть PHP. При обращении к полям $this или static обязательно.
По большому счёту, это непринципиально. Пока у нас есть однозначные правила, а биндинг делается статически, всё будет работать и без костылей вроде $this.
В шарпе эта фича не приводит к сколь-нибудь заметному геморрою, зато позволяет в нужных местах писать код компактно.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.