Область видимости переменных
Область видимости переменной — это область кода, в которой переменная видна. Область видимости переменных помогает избежать конфликтов имён переменных. Концепция интуитивна: две функции могут иметь аргументы, названные 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.
Локальная область видимости
Новый локальный объём вводится большинством блоков кода (см. выше таблицу для полного списка). Локальная область видимости наследует все переменные из родительской локальной области видимости, как для чтения, так и для записи. Кроме того, локальная область видимости наследует все глобальные переменные, которые были присвоены в её родительском глобальном блоке области видимости (если она окружена глобальной if или begin областью видимости). В отличие от глобальных областей видимости, локальные области видимости не являются пространствами имён, поэтому переменные во внутренней области видимости не могут быть получены из родительской области видимости посредством какого-либо квалифицированного доступа.
Следующие правила и примеры относятся к локальным областям видимости. Новая переменная, введённая в локальной области видимости, не распространяется на родительскую область видимости. Например, здесь $z$ не вводится в верхний уровень:
julia> for i = 1:10
z = i
end
julia> z
ERROR: UndefVarError: z not defined
В этом и всех последующих примерах предполагается, что их верхний уровень является глобальной областью видимости с чистым рабочим пространством, например, недавно запущенный REPL.
Внутри локальной области видимости переменную можно принудительно сделать новой локальной переменной, используя ключевое слово local:
julia> x = 0;
julia> for i = 1:10
local x # this is also the default
x = i + 1
end
julia> x
0
Внутри локальной области видимости глобальной переменной можно присвоить значение, используя ключевое слово 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. В этом случае ключевое слово затрагивает все перечисленные переменные.
Локальные области видимости вводятся большинством ключевых слов блоков, за исключением begin и if.
В локальной области видимости все переменные наследуются из её родительского глобального блока, если:
- присваивание привело бы к изменению глобальной переменной, или
- переменная специально помечена ключевым словом
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
Переменная цикла или итерации в цикле 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–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.0.4/manual/variables-and-scoping/