Документация Engee

Область видимости переменных

Областью видимости (или для краткости: областью) переменной называется та часть кода, в пределах которой переменная доступна. Ограничение областей позволяет избежать конфликтов имен переменных. Смысл очень простой: у двух разных функций могут быть аргументы с одним и тем же именем x, и эти два аргумента x ссылаются на разные объекты. Возможно и множество других ситуаций, когда в разных блоках кода одно и то же имя имеет разное значение. То, в каких случаях переменные с одним и тем же именем имеют одинаковое или разное значение, зависит от правил определения области. В этом разделе они разбираются подробно.

С помощью определенных конструкций языка образуются блоки областей, то есть части кода, которые могут выступать в роли областей для некоторых наборов переменных. Областью переменной не может быть произвольный набор строк исходного кода; область обязательно должна соответствовать одному из таких блоков. В Julia есть два основных типа областей: глобальная и локальная. Второй тип может быть вложенным. В Julia также различаются конструкции, с помощью которых вводятся строгие области в противоположность нестрогим. От этого зависит возможность затенения глобальной переменной тем же именем.

Конструкции областей видимости

Блоки областей создаются с помощью следующих конструкций.

Конструкция Тип области Область, в которой можно использовать

module, baremodule

Глобальная

Глобальная

struct

Локальная (нестрогая)

Глобальная

for, while, try

Локальная (нестрогая)

Глобальная, локальная

macro

Локальная (строгая)

Глобальная

Функции, блоки do, блоки let, включения, генераторы

Локальная (строгая)

Глобальная, локальная

Примечательно, что в этой таблице отсутствуют блоки 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
       end;

julia> module B
           module C
               c = 2
           end
           b = C.c    # Доступ к пространству имен вложенной глобальной области
                      # возможен по квалифицированным именам
           import ..A # делает доступным модуль A
           d = A.a
       end;

julia> module D
           b = a # вызывает ошибку, так как глобальная область D изолирована от A
       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 в этой части кода.

  1. Существующая локальная переменная: если x уже является локальной переменной, то значение присваивается этой существующей переменной x.

  2. Строгая область: если локальной переменной x еще не существует и присваивание происходит внутри конструкции, образующей строгую область (то есть внутри блока let, тела функции или макроса, включения или генератора), создается новая локальная переменная с именем x в области присваивания.

  3. Нестрогая область: если локальной переменной x еще не существует и все конструкции областей, в которые входит присваивание, являются нестрогими (циклы, блоки try иcatch или блоки struct), результат зависит от того, определена ли глобальная переменная x.

    • Если глобальная переменная x не определена, создается новая локальная переменная с именем x в области присваивания.

    • Если глобальная переменная x определена, присваивание считается неоднозначным.

      • В неинтерактивных контекстах (файлах, eval) выводится предупреждение о неоднозначности, и создается новая локальная переменная.

      • В интерактивных контекстах (REPL, блокнотах) значение присваивается глобальной переменной x.

Как можно заметить, в неинтерактивных контекстах правила для строгой и нестрогой областей одинаковые за тем исключением, что, если глобальная переменная затеняется неявной локальной (то есть не объявленной как local x), выводится предупреждение. В интерактивных контекстах в целях удобства действуют более сложные правила. Более подробно они будут разобраны в дальнейших примерах.

Теперь, когда правила вам известны, давайте рассмотрим ряд примеров. Предполагается, что каждый пример выполняется в новом сеансе REPL, так что в каждом фрагменте кода значения присваиваются только глобальным переменным в данном блоке.

Начнем с простой и ясной ситуации — присваивания внутри строгой области, в данном случае внутри тела функции, когда локальной переменной с таким именем еще не существует:

julia> function greet()
           x = "hello" # Новая локальная переменная
           println(x)
       end
greet (generic function with 1 method)

julia> greet()
hello

julia> x # Глобальная
ERROR: UndefVarError: `x` not defined

Внутри функции greet присваивание x = "hello" приводит к созданию новой локальной переменной x в области функции. При этом значение имеют два факта: присваивание происходит в локальной области, и локальной переменной x еще нет. Так как x — локальная переменная, не важно, имеется ли глобальная переменная с именем x. Здесь для примера мы выполняем присваивание x = 123 перед определением и вызовом greet.

julia> x = 123 # Глобальная
123

julia> function greet()
           x = "hello" # Новая локальная переменная
           println(x)
       end
greet (generic function with 1 method)

julia> greet()
hello

julia> x # Глобальная
123

Так как переменная x в greet локальная, вызов greet не влияет на значение глобальной переменной x (и неважно, существует ли она). Согласно правилу для строгой области, существование глобальной переменной с именем x не учитывается: присваивание значения переменной x в строгой области происходит на локальном уровне (если только переменная x не объявлена как глобальная).

В следующей очевидной ситуации, которую мы рассмотрим, локальная переменная с именем x уже существует. В таком случае оператор x = <value> всегда присваивает значение этой существующей локальной переменной x. Это верно независимо от того, происходит ли присваивание в той же локальной области, во внутренней локальной области в теле той же функции или в теле функции, вложенной в другую функцию, что также известно как замыкание.

Для примера мы используем функцию sum_to, которая подсчитывает сумму целых чисел от одного до n.

function sum_to(n)
    s = 0 # Новая локальная переменная
    for i = 1:n
        s = s + i # Значение присваивается существующей локальной переменной
    end
    return s # Та же локальная переменная
end

Как и в предыдущем примере, первое присваивание переменной s в начале функции sum_to приводит к объявлению новой локальной переменной s в теле функции. У цикла for собственная внутренняя локальная область в пределах области функции. В месте, где происходит присваивание s = s + i, локальная переменная s уже существует, поэтому присваивание приводит к изменению значения существующей переменной s, а не к созданию новой локальной переменной. Это можно проверить, вызвав sum_to в REPL.

julia> function sum_to(n)
           s = 0 # Новая локальная переменная
           for i = 1:n
               s = s + i # Значение присваивается существующей локальной переменной
           end
           return s # Та же локальная переменная
       end
sum_to (generic function with 1 method)

julia> sum_to(10)
55

julia> s # Глобальная
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 # Новая локальная переменная
           for i = 1:n
               t = s + i # Новая локальная переменная `t`
               s = t # Значение присваивается существующей локальной переменной `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 # Новая локальная переменная `t`
               s = t # Значение присваивается той же локальной переменной `s`, что и ниже
           end
           s = 0 # Новая локальная переменная
           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)

Этот пример иллюстрирует пару важных моментов.

  1. Внутренние области функций аналогичны любой другой вложенной локальной области. В частности, если переменная уже является локальной вне внутренней функции и ей присваивается значение во внутренней функции, изменяется внешняя локальная переменная.

  2. Не имеет значения, находится ли определение внешней локальной переменной после того места, где переменная обновляется, — правило остается тем же. Перед определением значения внутренних локальных переменных анализируется вся внешняя локальная область и разрешаются ее локальные переменные.

Такая схема означает, что обычно код можно переносить во внутреннюю функцию или из нее, не меняя его смысла, что упрощает использование ряда стандартных идиом в языке, поддерживающем замыкания (см. раздел, посвященный блокам do).

Давайте перейдем к ряду более запутанных случаев, где применяется правило нестрогой области. Для этого мы извлечем тела функций greet и sum_to_def в контекст нестрогой области. Сначала давайте поместим тело функции greet в цикл for, который образует нестрогую область, и выполним его в REPL.

julia> for i = 1:3
           x = "hello" # Новая локальная переменная
           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 # Глобальная
0

julia> for i = 1:10
           t = s + i # Новая локальная переменная `t`
           s = t # Значение присваивается глобальной переменной `s`
       end

julia> s # Глобальная
55

julia> @isdefined(t) # Глобальная
false

В REPL выполнение в теле функции имитируется путем определения того, приводит ли присваивание внутри цикла к изменению значения глобальной переменной или к созданию новой локальной переменной, на основе того, определена ли глобальная переменная с таким именем. Если глобальная переменная с таким именем существует, присваивание приводит к ее изменению. Если глобальной переменной нет, в результате присваивания создается новая локальная переменная. В этом примере демонстрируются оба случая.

  • Глобальной переменной с именем t нет, поэтому в результате присваивания t = s + i создается новая переменная t, локальная для цикла for.

  • Глобальная переменная с именем s существует, поэтому выражение s = t присваивает ей значение.

Вторым фактом объясняется то, почему в результате выполнения цикла изменяется значение глобальной переменной s, а первым — то, почему переменная t по-прежнему не определена после выполнения цикла. Теперь давайте попробуем выполнить тот же код так, как если бы он содержался в файле.

julia> code = """
       s = 0 # Глобальная
       for i = 1:10
           t = s + i # Новая локальная переменная `t`
           s = t # Новая локальная переменная `s` с предупреждением
       end
       s, # Глобальная
       @isdefined(t) # Глобальная
       """;

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 во второй строке — локальной, но это невозможно, так как эти две строки находятся в одном блоке области, а в пределах определенной области каждая переменная может иметь только один смысл.

Подробнее о нестрогих областях видимости

Мы рассмотрели все правила для локальных областей, но, прежде чем завершать этот раздел, стоит сказать несколько слов о том, почему нестрогая область действует по-разному в интерактивном и неинтерактивном контексте. Возникают два очевидных вопроса.

  1. Почему не везде такое поведение, как в REPL?

  2. Почему не везде такое поведение, как в файлах? Может быть, предупреждение можно просто игнорировать?

В версиях 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

# гораздо дальше
# возможно, в другом файле

for i = 1:10
    x = "hello"
    println(x)
end

# гораздо дальше
# возможно, еще в одном файле
# или, может быть, в предыдущем, где `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 в таком коде верхнего уровня чревато двумя основными проблемами.

  1. Теперь не так удобно копировать код из тела функции в REPL для отладки — приходится добавлять аннотации global, а затем удалять их при переносе кода обратно.

  2. Неопытные разработчики могут писать такой код без ключевого слова 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 — локальная переменная, y — глобальная
           println("z: $z") # выдает ошибку, так как переменной z еще не присвоено значение, но она является локальной
       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
               # пусто
           end
           return i
       end;

julia> f()
0

Однако иногда бывает полезно повторно использовать существующую локальную переменную как переменную итерации. Для этого удобно использовать ключевое слово outer.

julia> function f()
           i = 0
           for outer i = 1:3
               # пусто
           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

Поддержка типизированных глобальных переменных была добавлена в 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:
[...]