Scope of Variables

Область видимости переменной — это область кода, в которой переменная доступна. Область видимости переменных помогает избежать конфликтов имен переменных. Концепция интуитивно понятна: две функции могут иметь аргументы с именем x, не ссылаясь при этом на одно и то же. Аналогично, есть много других случаев, когда разные блоки кода могут использовать одно и то же имя, не ссылаясь на одно и то же. Правила, определяющие, когда одно и то же имя переменной ссылается или не ссылается на одно и то же, называются правилами области видимости; в этом разделе они подробно изложены.

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

Scope constructs

Конструкции, вводящие блоки области видимости, это:

ConstructScope typeAllowed within
module, baremoduleglobalglobal
structlocal (soft)global
for, while, trylocal (soft)global, local
macrolocal (hard)global
functions, do blocks, let blocks, comprehensions, generatorslocal (hard)global, local

Заметно отсутствуют в этой таблице begin blocks и if blocks, которые не вводят новые области. Три типа областей следуют несколько различным правилам, которые будут объяснены ниже.

Джулия использует lexical scoping, что означает, что область видимости функции не наследует от области видимости вызывающего, а от области, в которой функция была определена. Например, в следующем коде x внутри foo ссылается на x в глобальной области видимости его модуля Bar:

julia> module Bar
           x = 1
           foo() = x
       end;

и нет x в области видимости, где используется foo:

julia> import .Bar

julia> x = -1;

julia> Bar.foo()
1

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

Global Scope

Каждый модуль вводит новую глобальную область видимости, отдельную от глобальной области видимости всех других модулей — не существует всеобъемлющей глобальной области видимости. Модули могут вводить переменные других модулей в свою область видимости через using or import операторы или через квалифицированный доступ с использованием точечной нотации, т.е. каждый модуль является так называемым пространством имен, а также структурой данных первого класса, ассоциирующей имена со значениями.

Если выражение верхнего уровня содержит объявление переменной с ключевым словом local, то эта переменная недоступна за пределами этого выражения. Переменная внутри выражения не влияет на глобальные переменные с тем же именем. Примером может служить объявление local x в блоке begin или if на верхнем уровне:

julia> x = 1
       begin
           local x = 0
           @show x
       end
       @show x;
x = 0
x = 1

Обратите внимание, что интерактивный запрос (также известный как REPL) находится в глобальной области видимости модуля Main.

Local Scope

Новая локальная область видимости вводится большинством блоков кода (см. выше table для полного списка). Если такой блок синтаксически вложен внутри другой локальной области видимости, область, которую он создает, вложена во все локальные области, в которых он появляется, которые в конечном итоге вложены в глобальную область видимости модуля, в котором код оценивается. Переменные во внешних областях видимости видны из любой области, которую они содержат — это означает, что их можно читать и записывать во внутренних областях — если только нет локальной переменной с тем же именем, которая "затеняет" внешнюю переменную с тем же именем. Это верно даже если внешняя локальная переменная объявлена после (в смысле текстуально ниже) внутреннего блока. Когда мы говорим, что переменная "существует" в данной области видимости, это означает, что переменная с таким именем существует в любой из областей, в которых текущая область вложена, включая текущую.

Некоторые языки программирования требуют явного объявления новых переменных перед их использованием. Явное объявление работает и в 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" # new local
           println(x)
       end
greet (generic function with 1 method)

julia> greet()
hello

julia> x # global
ERROR: UndefVarError: `x` not defined in `Main`

Внутри функции 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. Это верно, независимо от того, происходит ли присваивание в том же локальном контексте, во внутреннем локальном контексте в том же теле функции или в теле функции, вложенной в другую функцию, также известной как closure.

Мы будем использовать функцию 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 in `Main`

Поскольку 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)

Этот пример иллюстрирует несколько ключевых моментов:

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

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

Давайте перейдем к некоторым более неоднозначным случаям, охваченным правилом мягкой области видимости. Мы исследуем это, извлекая тела функций 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 in `Main`

Поскольку глобальная 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 in local scope

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

On Soft Scope

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

  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

# 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, который работал нормально, но позже, когда кто-то другой добавляет новую глобальную переменную далеко — возможно, в другом файле — код внезапно меняет свое значение и либо ломается шумно, либо, что еще хуже, молча делает что-то неправильное. Этот вид "spooky action at a distance" — это то, что хорошие дизайны языков программирования должны предотвращать.

Итак, в 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 notebooks (так же, как и Julia 0.6), а в файлах и других неинтерактивных контекстах он выводит это очень прямое предупреждение:

Присвоение s в мягкой области видимости неоднозначно, потому что существует глобальная переменная с тем же именем: s будет рассматриваться как новая локальная. Устраните неоднозначность, используя local s, чтобы подавить это предупреждение, или global s, чтобы присвоить значение существующей глобальной переменной.

Это решает обе проблемы, сохраняя при этом преимущества поведения 1.0 в "программировании в масштабе": глобальные переменные не оказывают странного влияния на значение кода, который может находиться далеко; в REPL копирование и вставка для отладки работают, и у новичков нет никаких проблем; каждый раз, когда кто-то забывает аннотацию global или случайно затеняет существующую глобальную переменную локальной в мягкой области видимости, что в любом случае было бы запутанным, они получают четкое предупреждение.

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

Let Blocks

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 in local scope

Присваивания оцениваются по порядку, при этом каждая правая часть оценивается в области видимости до того, как новая переменная с левой стороны была введена. Поэтому имеет смысл написать что-то вроде 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

Loops and Comprehensions

In loops and comprehensions, new variables introduced in their body scopes are freshly allocated for each loop iteration, as if the loop body were surrounded by a let block, as demonstrated by this example:

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

Constants

Общее использование переменных заключается в присвоении имен конкретным, неизменным значениям. Такие переменные присваиваются только один раз. Это намерение можно передать компилятору, используя ключевое слово 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

Typed Globals

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

Для любого присваивания глобальной переменной, Джулия сначала попытается преобразовать его в соответствующий тип, используя 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:
[...]