Spec-Zone.ru › Julia 1.10

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

Область видимости переменной — это область кода, в которой переменная доступна. Область видимости переменных помогает избежать конфликтов имён переменных. Концепция интуитивна: две функции могут обе иметь аргументы, называемые 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 или через квалифицированный доступ с использованием записи с точкой, т.е. каждый модуль является так называемым пространством имён, а также структурой данных первого класса, связывающей имена со значениями.

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

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

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

Локальная область видимости

Новая локальная область видимости вводится большинством блоков кода (см. выше таблицу 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

Внутри функции 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)

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

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

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

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

О мягкой области видимости

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

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

В Julia ≤ 0.6 все глобальные области видимости работали так же, как и текущий REPL: когда происходило x = <value> в цикле (или try/catch, или struct тело), но вне тела функции (или let блока или понимания), принималось решение о том, должна ли 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 мы упростили правила области видимости: в любой локальной области видимости присваивание имени, которое не было уже локальной переменной, создавало новую локальную переменную. Это полностью устранило понятие мягкой области видимости, а также потенциальную возможность «spooky action». Мы обнаружили и исправили значительное количество ошибок из-за удаления мягкой области видимости, подтверждая выбор избавиться от неё. И многие радовались! Ну, не очень. Потому что некоторые люди злились, что теперь им нужно писать:

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 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–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.10/manual/variables-and-scoping/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API