Область видимости переменных
Область видимости переменной — это область кода, в которой переменная доступна. Область видимости переменных помогает избежать конфликтов имён переменных. Концепция интуитивна: две функции могут иметь аргументы, названные x без того, чтобы два x ссылались на одну и ту же вещь. Аналогично, существуют и другие случаи, когда разные блоки кода могут использовать одно и то же имя без ссылки на одну и ту же вещь. Правила, определяющие, ссылается ли одно и то же имя переменной на одну и ту же вещь или нет, называются правилами области видимости; в этом разделе они подробно изложены.
Некоторые конструкции языка вводят блоки области видимости, которые представляют собой области кода, которые могут быть областью видимости некоторого набора переменных. Область видимости переменной не может быть произвольным набором строк исходного кода; вместо этого она всегда соответствует одному из этих блоков. В Julia существуют два основных типа областей видимости: глобальная область видимости и локальная область видимости. Последняя может быть вложенной. В Julia также существует различие между конструкциями, которые вводят «жёсткую область видимости», и теми, которые вводят только «мягкую область видимости», что влияет на то, разрешено ли затенение глобальной переменной с тем же именем или нет.
Конструкции области видимости
Конструкции, вводящие блоки области видимости:
| Конструкция | Тип области видимости | Разрешено внутри |
|---|---|---|
module, baremodule
|
глобальная | глобальная |
struct |
локальная (мягкая) | глобальная |
for, while, try
|
локальная (мягкая) | глобальная, локальная |
macro |
локальная (жёсткая) | глобальная |
функции, блоки do, блоки let, comprehension, генераторы |
локальная (жёсткая) | глобальная, локальная |
Обратите внимание, что в этой таблице отсутствуют блоки begin и if, которые не вводят новые области видимости. Три типа областей видимости подчиняются несколько разным правилам, которые будут объяснены ниже.
Julia использует лексическую область видимости, что означает, что область видимости функции не наследуется от области видимости вызывающей функции, а от области видимости, в которой функция была определена. Например, в следующем коде x внутри foo относится к x в глобальной области видимости её модуля Bar:
julia> module Bar
x = 1
foo() = x
end;
а не к x в области видимости, где используется foo:
julia> import .Bar julia> x = -1; julia> Bar.foo() 1
Таким образом, лексическая область видимости означает, что то, на что переменная в конкретном фрагменте кода ссылается, может быть выведено из кода, в котором она появляется, и не зависит от того, как выполняется программа. Область видимости, вложенная внутри другой области видимости, может «видеть» переменные во всех внешних областях видимости, в которых она содержится. Внешние области видимости, в свою очередь, не могут видеть переменные во внутренних областях видимости.
Глобальная область видимости
Каждый модуль вводит новую глобальную область видимости, отделённую от глобальной области видимости всех других модулей — нет всеобъемлющей глобальной области видимости. Модули могут вводить переменные других модулей в свою область видимости с помощью операторов using или import или с помощью квалифицированного доступа, используя нотацию точек, т.е. каждый модуль является так называемым пространством имён, а также структурой данных первого класса, сопоставляющей имена значениям. Обратите внимание, что хотя внешние переменные могут быть прочитаны, их можно изменить только внутри того модуля, к которому они относятся. Как крайний вариант, вы всегда можете оценить код внутри этого модуля, чтобы изменить переменную; это гарантирует, что привязки к модулям не могут быть изменены внешним кодом, который никогда не вызывает eval.
julia> module A
a = 1 # a global in A's scope
end;
julia> module B
module C
c = 2
end
b = C.c # can access the namespace of a nested global scope
# through a qualified access
import ..A # makes module A available
d = A.a
end;
julia> module D
b = a # errors as D's global scope is separate from A's
end;
ERROR: UndefVarError: `a` not defined
Если выражение верхнего уровня содержит объявление переменной с ключевым словом local, то эта переменная недоступна за пределами этого выражения. Переменная внутри выражения не влияет на глобальные переменные с тем же именем. Примером является объявление local x в блоке begin или if на верхнем уровне:
julia> x = 1
begin
local x = 0
@show x
end
@show x;
x = 0
x = 1
Обратите внимание, что интерактивный командный интерпретатор (также известный как REPL) находится в глобальной области видимости модуля Main.
Локальная область видимости
Новая локальная область видимости вводится большинством блоков кода (см. выше таблицу таблицу для полного списка). Если такой блок синтаксически вложен внутри другой локальной области видимости, создаваемая им область видимости вкладывается во все локальные области видимости, внутри которых она находится, которые в конечном итоге все вложены в глобальную область видимости модуля, в котором код оценивается. Переменные во внешних областях видимости видны из любой содержащей их области видимости — это означает, что их можно читать и записывать во внутренних областях видимости — за исключением случаев, когда существует локальная переменная с тем же именем, что и внешняя переменная с тем же именем, которая «затеняет» внешнюю переменную с тем же именем. Это верно даже если внешняя локальная переменная объявлена после (в текстовом смысле ниже) внутреннего блока. Когда мы говорим, что переменная «существует» в данной области видимости, это означает, что переменная с таким именем существует в любой из областей видимости, в которую текущая область видимости вложена, включая текущую.
Некоторые языки программирования требуют явного объявления новых переменных перед их использованием. Явное объявление работает и в Julia: в любой локальной области видимости запись local x объявляет новую локальную переменную в этой области видимости, независимо от того, существует ли переменная с именем x во внешней области видимости или нет. Однако объявление каждой новой переменной таким образом довольно громоздко и утомительно, поэтому Julia, как и многие другие языки, рассматривает присваивание имени переменной, которая ещё не существует, как неявное объявление этой переменной. Если текущая область видимости глобальная, то новая переменная глобальная; если текущая область видимости локальная, то новая переменная локальная для самой внутренней локальной области видимости и будет видна внутри этой области видимости, но не снаружи. Если вы присваиваете значение существующей локальной переменной, то всегда обновляется эта существующая локальная переменная: вы можете затенить локальную переменную только явно объявив новую локальную переменную во вложенной области видимости с ключевым словом local. В частности, это относится к переменным, присвоенным во вложенных функциях, что может удивить пользователей, приходящих из Python, где присвоение во вложенной функции создаёт новую локальную переменную, если переменная не объявлена явно как нелокальная.
В основном это довольно интуитивно понятно, но, как и во многих вещах, которые интуитивно понятны, детали более тонки, чем можно было бы наивно предположить.
Когда x = <value> происходит в локальной области видимости, Julia применяет следующие правила, чтобы определить, что означает выражение, в зависимости от того, где происходит операция присваивания, и на что x уже ссылается в этой позиции:
-
Существующая локальная переменная: Если
xуже является локальной переменной, то существующей локальной переменнойxприсваивается значение; -
Жёсткая область видимости: Если
xещё не является локальной переменной и присваивание происходит внутри любой конструкции с жёсткой областью видимости (т.е. внутри блокаlet, тела функции или макроса, comprehension или генератора), создаётся новая локальная переменная с именемxв области видимости присваивания; -
Мягкая область видимости: Если
xещё не является локальной переменной и все конструкции области видимости, содержащие присваивание, являются мягкими областями видимости (циклы, блокиtry/catch, или блокиstruct), поведение зависит от того, определена ли глобальная переменнаяx:- если глобальная переменная
xне определена, создаётся новая локальная переменная с именемxв области видимости присваивания; - если глобальная переменная
xопределена, присваивание считается неоднозначным:- в неинтерактивных контекстах (файлы, eval) выводится предупреждение об неоднозначности, и создаётся новая локальная переменная;
- в интерактивных контекстах (REPL, ноутбуки) глобальной переменной
xприсваивается значение.
- если глобальная переменная
Вы можете заметить, что в неинтерактивных контекстах поведение жёсткой и мягкой области видимости идентично, за исключением того, что выводится предупреждение, когда неявно локальная переменная (т.е. не объявлена с local x) затеняет глобальную. В интерактивных контекстах правила следуют более сложной эвристике для удобства. Это подробно рассмотрено в примерах ниже.
Теперь, когда вы знаете правила, давайте посмотрим на некоторые примеры. Предполагается, что каждый пример оценивается в новой сессии REPL, чтобы единственными глобальными переменными в каждом фрагменте кода были переменные, присвоенные в этом блоке кода.
Начнём с ясной и понятной ситуации — присваивание внутри жёсткой области видимости, в данном случае тела функции, когда локальная переменная с таким именем ещё не существует:
julia> function greet()
x = "hello" # new local
println(x)
end
greet (generic function with 1 method)
julia> greet()
hello
julia> x # global
ERROR: UndefVarError: `x` not defined
Внутри функции greet присваивание x = "hello" приводит к тому, что x становится новой локальной переменной в области видимости функции. Существуют два важных факта: присваивание происходит в локальной области видимости и нет существующей локальной переменной x. Поскольку x локальная, не имеет значения, существует ли глобальная переменная с именем x или нет. Например, здесь мы определяем x = 123 перед определением и вызовом greet:
julia> x = 123 # global
123
julia> function greet()
x = "hello" # new local
println(x)
end
greet (generic function with 1 method)
julia> greet()
hello
julia> x # global
123
Поскольку x в greet локальная, значение (или его отсутствие) глобальной переменной x не изменяется при вызове greet. Правило жёсткой области видимости не учитывает, существует ли глобальная переменная с именем x или нет: присваивание x в жёсткой области видимости является локальным (если x объявлена глобальной).
Следующая ясная ситуация, которую мы рассмотрим, это когда уже существует локальная переменная, названная x, в этом случае x = <value> всегда присваивает значение этой существующей локальной x. Это верно, независимо от того, происходит ли присвоение в том же локальном пространстве, во внутреннем локальном пространстве в том же теле функции или в теле функции, вложенной в другую функцию, также известной как замыкание.
Мы будем использовать функцию sum_to, которая вычисляет сумму целых чисел от единицы до n, в качестве примера:
function sum_to(n)
s = 0 # new local
for i = 1:n
s = s + i # assign existing local
end
return s # same local
end
Как и в предыдущем примере, первое присвоение s в верхней части sum_to приводит к тому, что s становится новой локальной переменной в теле функции. У цикла for есть собственное внутреннее локальное пространство внутри пространства функции. В тот момент, когда происходит s = s + i, s уже является локальной переменной, поэтому присвоение обновляет существующую s вместо создания новой локальной. Мы можем проверить это, вызвав sum_to в REPL:
julia> function sum_to(n)
s = 0 # new local
for i = 1:n
s = s + i # assign existing local
end
return s # same local
end
sum_to (generic function with 1 method)
julia> sum_to(10)
55
julia> s # global
ERROR: UndefVarError: `s` not defined
Поскольку s локальна для функции sum_to, вызов функции не влияет на глобальную переменную s. Мы также видим, что обновление s = s + i в цикле for должно было обновить ту же s, созданную инициализацией s = 0, поскольку мы получаем правильную сумму 55 для целых чисел от 1 до 10.
Давайте углубимся в тот факт, что тело цикла for имеет собственное пространство, написав немного более подробную вариацию, которую мы назовем sum_to_def, в которой мы сохраняем сумму s + i в переменной t перед обновлением s.
julia> function sum_to_def(n)
s = 0 # new local
for i = 1:n
t = s + i # new local `t`
s = t # assign existing local `s`
end
return s, @isdefined(t)
end
sum_to_def (generic function with 1 method)
julia> sum_to_def(10)
(55, false)
Эта версия возвращает s как и прежде, но она также использует макрос @isdefined для возврата булевого значения, указывающего, существует ли локальная переменная, названная t, определенная во внешнем локальном пространстве функции. Как вы можете видеть, t не определен вне тела цикла for. Это связано с правилом жёсткого пространства: поскольку присвоение t происходит внутри функции, что вводит жёсткое пространство, присвоение приводит к тому, что t становится новой локальной переменной в локальном пространстве, где оно появляется, т. е. внутри тела цикла. Даже если бы существовала глобальная переменная, названная t, это не имело бы значения — правило жёсткого пространства не зависит ни от чего в глобальном пространстве.
Обратите внимание, что локальное пространство тела цикла for ничем не отличается от локального пространства вложенной функции. Это означает, что мы могли бы переписать этот пример таким образом, чтобы тело цикла было реализовано как вызов вложенной вспомогательной функции, и оно работало бы таким же образом:
julia> function sum_to_def_closure(n)
function loop_body(i)
t = s + i # new local `t`
s = t # assign same local `s` as below
end
s = 0 # new local
for i = 1:n
loop_body(i)
end
return s, @isdefined(t)
end
sum_to_def_closure (generic function with 1 method)
julia> sum_to_def_closure(10)
(55, false)
Этот пример иллюстрирует несколько ключевых моментов:
Вложенные пространства функций такие же, как и любое другое вложенное локальное пространство. В частности, если переменная уже является локальной переменной вне вложенной функции, и вы присваиваете ей значение во вложенной функции, то внешняя локальная переменная обновляется.
Неважно, происходит ли определение внешней локальной переменной ниже, чем её обновление, правило остается прежним. Всё окружающее локальное пространство анализируется, и его локальные переменные определяются до того, как будут разрешены внутренние локальные значения.
Этот дизайн означает, что вы, как правило, можете перемещать код в или из вложенной функции, не изменяя его смысл, что облегчает реализацию ряда распространённых идиом в языке, использующих замыкания (см. блоки do).
Перейдём к некоторым более неоднозначным случаям, охватываемым правилом мягкого пространства. Мы изучим это, извлекая тела функций greet и sum_to_def в контексты мягкого пространства. Сначала давайте поместим тело greet в цикл for — который является мягким, а не жёстким — и оценим его в REPL:
julia> for i = 1:3
x = "hello" # new local
println(x)
end
hello
hello
hello
julia> x
ERROR: UndefVarError: `x` not defined
Так как глобальная переменная x не определена, когда оценивается цикл for, выполняется первая часть правила мягкого пространства, и x создаётся как локальная переменная для цикла for, и поэтому глобальная переменная x остаётся неопределённой после выполнения цикла. Далее, рассмотрим тело sum_to_def и перенесём его в глобальное пространство, зафиксировав его аргумент на n = 10
s = 0
for i = 1:10
t = s + i
s = t
end
s
@isdefined(t)
Что делает этот код? Подсказка: это вопрос-ловушка. Ответ: "это зависит". Если этот код вводится интерактивно, он ведёт себя так же, как и в теле функции. Но если код появляется в файле, он выводит предупреждение об неоднозначности и вызывает ошибку неопределённой переменной. Давайте сначала посмотрим, как это работает в REPL:
julia> s = 0 # global
0
julia> for i = 1:10
t = s + i # new local `t`
s = t # assign global `s`
end
julia> s # global
55
julia> @isdefined(t) # global
false
REPL приближённо моделирует нахождение внутри тела функции, определяя, присваивает ли присвоение внутри цикла значение глобальной переменной или создаёт новую локальную переменную, в зависимости от того, определена ли глобальная переменная с этим именем или нет. Если глобальная переменная с таким именем существует, то присвоение обновляет её. Если глобальная переменная не существует, то присвоение создаёт новую локальную переменную. В этом примере мы видим оба случая в действии:
- Глобальная переменная с именем
tне определена, поэтомуt = s + iсоздаёт новуюt, которая является локальной для циклаfor; - Глобальная переменная с именем
sопределена, поэтомуs = tприсваивает значение ей.
Второй факт объясняет, почему выполнение цикла изменяет глобальное значение s, а первый факт объясняет, почему t всё ещё неопределён после выполнения цикла. Теперь давайте попробуем оценить тот же код, как будто он находится в файле:
julia> code = """
s = 0 # global
for i = 1:10
t = s + i # new local `t`
s = t # new local `s` with warning
end
s, # global
@isdefined(t) # global
""";
julia> include_string(Main, code)
┌ Warning: Assignment to `s` in soft scope is ambiguous because a global variable by the same name exists: `s` will be treated as a new local. Disambiguate by using `local s` to suppress this warning or `global s` to assign to the existing global variable.
└ @ string:4
ERROR: LoadError: UndefVarError: `s` not defined
Здесь мы используем include_string, чтобы оценить code так, как будто это содержимое файла. Мы также могли бы сохранить code в файле, а затем вызвать include на этом файле — результат был бы таким же. Как вы можете видеть, это ведёт себя совершенно по-другому, чем оценивание того же кода в REPL. Давайте разберём, что происходит здесь:
- глобальная переменная
sопределена со значением0до оценки цикла - присвоение
s = tпроисходит в мягком пространстве — в циклеforвне любого тела функции или другой структуры жёсткого пространства - следовательно, выполняется вторая часть правила мягкого пространства, и присвоение является неоднозначным, поэтому выводится предупреждение
- выполнение продолжается, делая
sлокальной для тела циклаfor - поскольку
sлокальна для циклаfor, она неопределённа, когда оцениваетсяt = s + i, что вызывает ошибку - вычисление останавливается там, но если бы оно добралось до
sи@isdefined(t), оно бы вернуло0иfalse.
Это демонстрирует некоторые важные аспекты пространства: в пространстве каждая переменная может иметь только одно значение, и это значение определяется независимо от порядка выражений. Наличие выражения s = t в цикле приводит к тому, что s является локальной переменной для цикла, что означает, что она также является локальной, когда она появляется в правой части t = s + i, даже если это выражение появляется раньше и оценивается первым. Можно представить, что s на первой строке цикла может быть глобальной, а s на второй строке цикла — локальной, но это невозможно, так как две строки находятся в одном блоке пространства, и каждая переменная может иметь только одно значение в данном пространстве.
О мягком пространстве
Мы теперь рассмотрели все правила локального пространства, но прежде чем завершить этот раздел, возможно, стоит сказать несколько слов о том, почему неоднозначный случай мягкого пространства обрабатывается по-разному в интерактивных и неинтерактивных контекстах. Возникают два очевидных вопроса:
- Почему это не работает так же везде, как в REPL?
- Почему это не работает так же, как в файлах везде? И возможно, пропустить предупреждение?
В Julia ≤ 0.6 все глобальные пространства работали как текущий REPL: когда x = <value> встречался в цикле (или try/catch, или struct тела), но вне тела функции (или let блока или понимания), определялось, должно ли x быть локальным для цикла, в зависимости от того, определена ли глобальная переменная с именем x или нет. Эта функция имеет преимущество интуитивности и удобства, так как максимально приближена к поведению внутри тела функции. В частности, это упрощает перемещение кода между телом функции и REPL при отладке поведения функции. Однако, она имеет некоторые недостатки. Во-первых, это довольно сложное поведение: многие люди на протяжении многих лет были сбиты с толку этим поведением и жаловались, что оно сложное, сложно как объяснить, так и понять. Правильно. Во-вторых, и, возможно, хуже, это плохо для программирования "в масштабе". Когда вы видите небольшой фрагмент кода в одном месте, как это, довольно ясно, что происходит:
s = 0
for i = 1:10
s += i
end
Очевидно, целью является изменение существующей глобальной переменной s. Что ещё это может означать? Однако не весь реальный код такой короткий или понятный. Мы обнаружили, что код, подобный следующему, часто встречается в дикой природе:
x = 123
# much later
# maybe in a different file
for i = 1:10
x = "hello"
println(x)
end
# much later
# maybe in yet another file
# or maybe back in the first one where `x = 123`
y = x + 234
Гораздо менее очевидно, что должно произойти здесь. Поскольку x + "hello" — это ошибка метода, кажется вероятным, что цель состоит в том, чтобы x была локальной для цикла for. Но значения времени выполнения и методы, которые, возможно, существуют, не могут быть использованы для определения пространства переменных. С поведением Julia ≤ 0.6 особенно беспокоительно, что кто-то мог написать цикл for первым, заставив его работать, но позже, когда кто-то добавляет новую глобальную переменную далеко — возможно, в другом файле — код внезапно меняет смысл и либо ломается с ошибкой, либо, что ещё хуже, тихо выполняет неверные действия. Такая "сверхъестественная сила действия на расстоянии" — это то, чему хорошему программированию на языке следует препятствовать.
Таким образом, в Julia 1.0 мы упростили правила для пространства: в любом локальном пространстве присвоение имени, которое не было уже локальной переменной, создавало новую локальную переменную. Это полностью устранило понятие мягкого пространства, а также устранило потенциальную возможность сверхъестественного действия. Мы обнаружили и исправили значительное количество ошибок из-за удаления мягкого пространства, подтверждая выбор избавиться от него. И тут начался настоящий праздник! Ну, не совсем. Потому что некоторые люди были в ярости, что теперь им нужно было написать:
s = 0
for i = 1:10
global s += i
end
Видите аннотацию global там? Ужасно. Очевидно, что эта ситуация не могла быть терпимой. Но серьезно, есть две основные проблемы с требованием global для этого типа кода верхнего уровня:
Теперь неудобно копировать и вставлять код из тела функции в REPL для отладки — вам нужно добавить аннотации
globalи затем снова удалить их, чтобы вернуться;Начинающие программисты будут писать этот тип кода без
globalи не будут знать, почему их код не работает — ошибка, которую они получат, заключается в том, чтоsне определено, что, похоже, не проясняет ситуацию никому, кто совершает эту ошибку.
Начиная с Julia 1.5, этот код работает без аннотации global в интерактивных контекстах, таких как REPL или Jupyter-тетради (как в Julia 0.6), а в файлах и других неинтерактивных контекстах он выводит следующее прямое предупреждение:
Присвоение
sв мягкой области видимости неоднозначно, потому что глобальная переменная с тем же именем существует:sбудет обработана как новая локальная переменная. Устраните неоднозначность, используяlocal sдля подавления этого предупреждения илиglobal sдля присвоения существующей глобальной переменной.
Это решает обе проблемы, сохраняя при этом преимущества «программирования в масштабе» поведения 1.0: глобальные переменные не оказывают странного влияния на значение кода, который может находиться далеко; в REPL работает отладка методом копирования-вставки, и начинающие программисты не сталкиваются с проблемами; каждый раз, когда кто-то забывает аннотацию global или случайно затеняет существующую глобальную переменную локальной переменной в мягкой области видимости, что само по себе было бы запутанным, они получат ясное и понятное предупреждение.
Важным свойством этой конструкции является то, что любой код, который выполняется в файле без предупреждений, будет вести себя так же в свежем REPL. И наоборот, если вы сохраните сеанс REPL в файл, если он будет вести себя иначе, чем в REPL, то вы получите предупреждение.
Блоки Let
let операторы создают новый блок жесткой области видимости (см. выше) и вводят новые привязки переменных каждый раз, когда они выполняются. Переменная не обязательно должна быть немедленно присвоена:
julia> var1 = let x
for i in 1:5
(i == 4) && (x = i; break)
end
x
end
4
В то время как присвоения могут повторно назначать новое значение существующему расположению значения, let всегда создает новое расположение. Это различие обычно не имеет значения и обнаруживается только в случае переменных, которые переживают свой область видимости с помощью замыканий. Синтаксис let принимает последовательность присвоений и имен переменных, разделенных запятыми:
julia> x, y, z = -1, -1, -1;
julia> let x = 1, z
println("x: $x, y: $y") # x is local variable, y the global
println("z: $z") # errors as z has not been assigned yet but is local
end
x: 1, y: -1
ERROR: UndefVarError: `z` not defined
Присвоения оцениваются в порядке, при этом каждый правой операнд оценивается в области видимости, прежде чем новая переменная в левой части была введена. Поэтому имеет смысл записать что-то вроде let x = x, так как две переменные x различны и имеют отдельные области памяти. Вот пример, где необходимо поведение let.
julia> Fs = Vector{Any}(undef, 2); i = 1;
julia> while i <= 2
Fs[i] = ()->i
global i += 1
end
julia> Fs[1]()
3
julia> Fs[2]()
3
Здесь мы создаем и сохраняем два замыкания, которые возвращают переменную i. Однако, это всегда та же самая переменная i, поэтому два замыкания ведут себя идентично. Мы можем использовать let для создания новой привязки для i:
julia> Fs = Vector{Any}(undef, 2); i = 1;
julia> while i <= 2
let i = i
Fs[i] = ()->i
end
global i += 1
end
julia> Fs[1]()
1
julia> Fs[2]()
2
Поскольку конструкция begin не вводит новую область видимости, может быть полезно использовать let с нулевым аргументом, чтобы просто ввести новый блок области видимости без создания новых привязок сразу:
julia> let
local x = 1
let
local x = 2
end
x
end
1
Поскольку let вводит новый блок области видимости, внутренняя локальная переменная x — это другая переменная, чем внешняя локальная переменная x. Этот конкретный пример эквивалентен:
julia> let x = 1
let x = 2
end
x
end
1
Циклы и выражения-генераторы
В циклах и выражениях-генераторах новые переменные, введенные в области видимости их тела, выделяются заново для каждой итерации цикла, как если бы тело цикла было окружено блоком let, как показано в этом примере:
julia> Fs = Vector{Any}(undef, 2);
julia> for j = 1:2
Fs[j] = ()->j
end
julia> Fs[1]()
1
julia> Fs[2]()
2
Переменная цикла for или выражения-генератора всегда является новой переменной:
julia> function f()
i = 0
for i = 1:3
# empty
end
return i
end;
julia> f()
0
Однако иногда бывает полезно повторно использовать существующую локальную переменную в качестве переменной цикла. Это можно удобно сделать, добавив ключевое слово outer:
julia> function f()
i = 0
for outer i = 1:3
# empty
end
return i
end;
julia> f()
3
Константы
Распространенное использование переменных — присвоение имен конкретным неизменяемым значениям. Такие переменные присваиваются только один раз. Это намерение может быть передано компилятору с помощью ключевого слова const:
julia> const e = 2.71828182845904523536; julia> const pi = 3.14159265358979323846;
Несколько переменных могут быть объявлены в одном операторе const:
julia> const a, b = 1, 2 (1, 2)
Объявление const должно использоваться только в глобальной области видимости для глобальных переменных. Компилятору сложно оптимизировать код, включающий глобальные переменные, поскольку их значения (или даже их типы) могут изменяться практически в любой момент. Если глобальная переменная не будет изменяться, добавление объявления const решает эту проблему производительности.
Локальные константы существенно отличаются. Компилятор может автоматически определить, когда локальная переменная является константой, поэтому объявления локальных констант не требуются и на данный момент не поддерживаются.
Специальные присвоения верхнего уровня, такие как те, что выполняются ключевыми словами function и struct, по умолчанию являются константами.
Обратите внимание, что const влияет только на привязку переменной; переменная может быть привязана к изменяемому объекту (например, массиву), и этот объект по-прежнему может быть изменен. Кроме того, когда кто-то пытается присвоить значение переменной, объявленной как константа, возможны следующие сценарии:
- если новое значение имеет другой тип, чем тип константы, то выбрасывается ошибка:
julia> const x = 1.0 1.0 julia> x = 1 ERROR: invalid redefinition of constant x
- если новое значение имеет тот же тип, что и константа, то выводится предупреждение:
julia> const y = 1.0 1.0 julia> y = 2.0 WARNING: redefinition of constant y. This may fail, cause incorrect answers, or produce other errors. 2.0
- если присвоение не приведет к изменению значения переменной, сообщение не выдается:
julia> const z = 100 100 julia> z = 100 100
Последшее правило применяется к неизменяемым объектам, даже если привязка переменной изменится, например:
julia> const s1 = "1"
"1"
julia> s2 = "1"
"1"
julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
Ptr{UInt8} @0x00000000132c9638
Ptr{UInt8} @0x0000000013dd3d18
julia> s1 = s2
"1"
julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
Ptr{UInt8} @0x0000000013dd3d18
Ptr{UInt8} @0x0000000013dd3d18
Однако для изменяемых объектов предупреждение выводится, как ожидалось:
julia> const a = [1]
1-element Vector{Int64}:
1
julia> a = [1]
WARNING: redefinition of constant a. This may fail, cause incorrect answers, or produce other errors.
1-element Vector{Int64}:
1
Обратите внимание, что хотя иногда это возможно, изменение значения переменной const категорически не рекомендуется и предназначено только для удобства при интерактивном использовании. Изменение констант может привести к различным проблемам или неожиданному поведению. Например, если метод ссылается на константу и уже скомпилирован до изменения константы, то он может продолжать использовать старое значение:
julia> const x = 1 1 julia> f() = x f (generic function with 1 method) julia> f() 1 julia> x = 2 WARNING: redefinition of constant x. This may fail, cause incorrect answers, or produce other errors. 2 julia> f() 1
Типизированные глобальные переменные
Поддержка типизированных глобальных переменных была добавлена в Julia 1.8
Аналогично объявлению как констант, глобальные привязки также могут быть объявлены как имеющие постоянный тип. Это можно сделать, не присваивая фактического значения, используя синтаксис global x::T или при присвоении как x::T = 123.
julia> x::Float64 = 2.718
2.718
julia> f() = x
f (generic function with 1 method)
julia> Base.return_types(f)
1-element Vector{Any}:
Float64
Для любого присвоения глобальной переменной Julia сначала попытается преобразовать ее в соответствующий тип, используя convert:
julia> global y::Int julia> y = 1.0 1.0 julia> y 1 julia> y = 3.14 ERROR: InexactError: Int64(3.14) Stacktrace: [...]
Тип не обязательно должен быть конкретным, но аннотации с абстрактными типами обычно имеют небольшую выгоду в плане производительности.
После присвоения значения или установки типа глобальной переменной, тип привязки больше не может быть изменен:
julia> x = 1 1 julia> global x::Int ERROR: cannot set type for global x. It already has a value or is already set to a different type. Stacktrace: [...]
© 2009–2023 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.9/manual/variables-and-scoping/