Область действия переменных
Область действия переменной — это область кода, в которой переменная видима. Область действия переменных помогает избежать конфликтов имён переменных. Концепция интуитивна: две функции могут иметь аргументы с именем x, не ссылаясь на одну и ту же вещь. Точно так же существуют многие другие случаи, когда разные блоки кода могут использовать одно и то же имя, не ссылаясь на одну и ту же вещь. Правила, определяющие, ссылаются ли одинаковые имена переменных на одну и ту же вещь или нет, называются правилами области действия; в этом разделе они подробно описаны.
Некоторые конструкции языка вводят блоки области действия, которые являются областями кода, которые могут быть областью действия некоторого набора переменных. Область действия переменной не может быть произвольным набором строк исходного кода; вместо этого она всегда будет соответствовать одному из этих блоков. Существуют два основных типа областей действия в Julia: глобальная область действия и локальная область действия. Последняя может быть вложенной. Конструкции, вводящие блоки области действия, следующие:
Конструкции области действия
| Конструкция | Тип области действия | Вложенные блоки области действия |
|---|---|---|
module, baremodule
|
глобальная | глобальная |
| интерактивный пригласительный интерфейс (REPL) | глобальная | глобальная |
(изменяемый) struct, macro
|
локальная | глобальная |
for, while, try-catch-finally, let
|
локальная | глобальная или локальная |
| функции (любой синтаксис, анонимные и блоки do) | локальная | глобальная или локальная |
| понимания, объединение функций broadcast | локальная | глобальная или локальная |
Обратите внимание, что в этой таблице отсутствуют блоки 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:
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
Переменная цикла или генератора списков всегда является новой переменной:
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.3.1/manual/variables-and-scoping/