Область видимости переменных
Область видимости переменной — это область кода, в которой переменная видна. Область видимости переменных помогает избежать конфликтов имён переменных. Концепция интуитивна: две функции могут иметь аргументы с именем x без того, чтобы эти два x ссылались на одно и то же. Аналогично, существуют и другие случаи, когда разные блоки кода могут использовать одно и то же имя, не ссылаясь при этом на одну и ту же вещь. Правила, определяющие, ссылаются ли одинаковые имена переменных на одну и ту же вещь или нет, называются правилами области видимости; в этом разделе они подробно описаны.
Определённые конструкции языка вводят блоки области видимости, которые представляют собой области кода, которые могут быть областью видимости некоторого набора переменных. Область видимости переменной не может быть произвольным набором строк исходного кода; вместо этого она всегда будет соответствовать одному из этих блоков. Существует два основных типа областей видимости в Julia: глобальная область видимости и локальная область видимости. Последняя может быть вложенной. Конструкции, вводящие блоки области видимости:
Конструкции области видимости
| Конструкции | Тип области видимости | Вложенные блоки области видимости |
|---|---|---|
module, baremodule
|
глобальная | глобальная |
| интерактивный приглашающий ввод (REPL) | глобальная | глобальная |
(изменяемый) struct, macro
|
локальная | глобальная |
for, while, try-catch-finally, let
|
локальная | глобальная или локальная |
| функции (любой синтаксис, анонимные и блоки do) | локальная | глобальная или локальная |
| сжатия, слияние функций вещания | локальная | глобальная или локальная |
Заметно отсутствуют в этой таблице блоки 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 или с помощью квалифицированного доступа с использованием нотации точек, то есть каждый модуль является так называемым пространством имён. Обратите внимание, что привязки переменных могут быть изменены только в пределах их глобальной области видимости, а не из внешнего модуля.
julia> module A
a = 1 # a global in A's scope
end;
julia> module B
module C
c = 2
end
b = C.c # can access the namespace of a nested global scope
# through a qualified access
import ..A # makes module A available
d = A.a
end;
julia> module D
b = a # errors as D's global scope is separate from A's
end;
ERROR: UndefVarError: a not defined
julia> module E
import ..A # make module A available
A.a = 2 # throws below error
end;
ERROR: cannot assign variables in other modules
Обратите внимание, что интерактивное приглашение (также известное как REPL) находится в глобальной области видимости модуля Main.
Локальная область видимости
Новая локальная область видимости вводится большинством блоков кода (см. выше таблицу для полного списка). Локальная область видимости наследует все переменные из родительской локальной области видимости, как для чтения, так и для записи. В отличие от глобальных областей видимости, локальные области видимости не являются пространствами имён, поэтому переменные во внутренней области видимости не могут быть получены из родительской области видимости каким-либо способом квалифицированного доступа.
Следующие правила и примеры относятся к локальным областям видимости. Новая переменная, введённая в локальной области видимости, не может быть использована в родительской области видимости. Например, здесь $z$ не вводится в верхний уровень:
julia> for i = 1:10
z = i
end
julia> z
ERROR: UndefVarError: z not defined
В этом и всех последующих примерах предполагается, что их верхний уровень — это глобальная область видимости с чистым рабочим пространством, например, недавно запущенный REPL.
Однако внутренние локальные области видимости могут обновлять переменные в своих родительских областях видимости:
julia> for i = 1:1
z = i
for j = 1:1
z = 0
end
println(z)
end
0
Внутри локальной области видимости переменная может быть принудительно определена как новая локальная переменная с помощью ключевого слова local:
julia> for i = 1:1
x = i + 1
for j = 1:1
local x = 0
end
println(x)
end
2
Внутри локальной области видимости глобальная переменная может быть присвоена с помощью ключевого слова global:
julia> for i = 1:10
global z
z = i
end
julia> z
10
Местоположение ключевых слов local и global внутри блока области видимости не имеет значения. Следующее эквивалентно последнему примеру (хотя стилистически хуже):
julia> for i = 1:10
z = i
global z
end
julia> z
10
Ключевые слова local и global также могут применяться к деструктурирующим присваиваниям, например, local x, y = 1, 2. В этом случае ключевое слово влияет на все перечисленные переменные.
В локальной области видимости все переменные наследуются из родительского глобального блока области видимости, за исключением случаев:
- присваивание приведет к изменению глобальной переменной, или
- переменная специально помечена ключевым словом
local.
Таким образом, глобальные переменные наследуются только для чтения, а не для записи:
julia> x, y = 1, 2;
julia> function foo()
x = 2 # assignment introduces a new local
return x + y # y refers to the global
end;
julia> foo()
4
julia> x
1
Для присваивания глобальной переменной требуется явное указание global:
Многие считают, что избегание изменения значений глобальных переменных является лучшей программистской практикой. Изменение значения глобальной переменной может привести к «действию на расстоянии», что усложняет понимание поведения программы. Именно поэтому блоки области видимости, вводящие локальную область видимости, требуют ключевого слова global для объявления намерения изменить глобальную переменную.
julia> x = 1;
julia> function foobar()
global x = 2
end;
julia> foobar();
julia> x
2
Обратите внимание, что вложенные функции могут изменять локальные переменные родительской области видимости:
julia> x, y = 1, 2;
julia> function baz()
x = 2 # introduces a new local
function bar()
x = 10 # modifies the parent's x
return x + y # y is global
end
return bar() + x # 12 + 10 (x is modified in call of bar())
end;
julia> baz()
22
julia> x, y # verify that global x and y are unchanged
(1, 2)
Причина, по которой разрешается изменять локальные переменные родительских областей видимости вложенным функциям, заключается в возможности создания closures с закрытым состоянием, например, переменной state в следующем примере:
julia> let state = 0
global counter() = (state += 1)
end;
julia> counter()
1
julia> counter()
2
См. также замыкания в примерах в следующих двух разделах. Переменная, такая как x в первом примере и state во втором, которая наследуется из окружающего охвата внутренней функцией, иногда называется захваченной переменной. Захваченные переменные могут представлять проблемы производительности, о которых говорится в разделах по производительности.
Различие между наследованием глобальной области видимости и вложенностью локальной области видимости может привести к некоторым небольшим различиям между функциями, определёнными в локальных и глобальных областях видимости для присваивания переменных. Рассмотрим изменение последнего примера путём перемещения bar в глобальную область видимости:
julia> x, y = 1, 2;
julia> function bar()
x = 10 # local, no longer a closure variable
return x + y
end;
julia> function quz()
x = 2 # local
return bar() + x # 12 + 2 (x is not modified)
end;
julia> quz()
14
julia> x, y # verify that global x and y are unchanged
(1, 2)
Обратите внимание, что вышеизложенные правила вложенности не относятся к определениям типов и макросов, так как они могут появляться только в глобальной области видимости. Существуют специальные правила области видимости, касающиеся вычисления аргументов функций по умолчанию и ключевых аргументов, которые описаны в разделе Функции.
Присваивание, вводящее переменную, используемую внутри определения функции, типа или макроса, не обязательно должно предшествовать её внутреннему использованию:
julia> f = y -> y + a; julia> f(3) ERROR: UndefVarError: a not defined Stacktrace: [...] julia> a = 1 1 julia> f(3) 4
Это поведение может показаться немного странным для обычной переменной, но оно позволяет использовать именованные функции — которые просто представляют собой обычные переменные, содержащие объекты функций — до их определения. Это позволяет определять функции в любом интуитивном и удобном порядке, а не принуждая к порядку сверху вниз или к необходимости предварительных объявлений, до тех пор, пока они не будут определены к моменту фактического вызова. В качестве примера здесь приведён неэффективный, взаимно рекурсивный способ проверки того, являются ли положительные целые числа чётными или нечётными:
julia> even(n) = (n == 0) ? true : odd(n - 1); julia> odd(n) = (n == 0) ? false : even(n - 1); julia> even(3) false julia> odd(3) true
Julia предоставляет встроенные, эффективные функции для проверки чётности и нечётности, называемые iseven и isodd, поэтому вышеприведённые определения следует рассматривать только как примеры области видимости, а не эффективной разработки.
Блоки let
В отличие от присваивания локальным переменным, операторы let каждый раз при выполнении выделяют новые привязки переменных. Присваивание изменяет существующее значение местоположения, а 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.
Циклы for и генераторы
for циклы, while циклы и генераторы ведут себя следующим образом: любые новые переменные, введенные в их областях видимости, выделяются заново для каждой итерации цикла, как будто тело цикла окружено 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: redefining constant y 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 Array{Int64,1}:
1
julia> a = [1]
WARNING: redefining constant a
1-element Array{Int64,1}:
1
Обратите внимание, что хотя иногда это возможно, изменение значения переменной const строго не рекомендуется и предназначено только для удобства при интерактивном использовании. Изменение констант может вызвать различные проблемы или неожиданное поведение. Например, если метод ссылается на константу и уже скомпилирован до изменения константы, он может продолжить использовать старое значение:
julia> const x = 1 1 julia> f() = x f (generic function with 1 method) julia> f() 1 julia> x = 2 WARNING: redefining constant x 2 julia> f() 1
© 2009–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.4.2/manual/variables-and-scoping/