Область видимости переменных
Область видимости переменной — это область кода, в которой переменная доступна. Область видимости переменных помогает избежать конфликтов имён переменных. Концепция интуитивно понятна: две функции могут иметь аргументы с одинаковым именем x, и при этом два x не будут ссылаться на одну и ту же переменную. Аналогично, существуют и другие случаи, когда разные блоки кода могут использовать одно и то же имя, не ссылаясь на одну и ту же переменную. Правила, определяющие, ссылаются ли одинаковые имена переменных на одну и ту же переменную или нет, называются правилами области видимости; в этом разделе они подробно описаны.
Некоторые конструкции языка вводят блоки области видимости, которые являются областями кода, которые могут быть областью видимости некоторого набора переменных. Область видимости переменной не может быть произвольным набором строк исходного кода; вместо этого она всегда будет соответствовать одному из этих блоков. Существуют два основных типа областей видимости в Julia: глобальная область видимости и локальная область видимости, последняя может быть вложенной. Конструкции, вводящие блоки области видимости:
| Имя области видимости | Блок/конструкция, вводящая этот вид области видимости |
|---|---|
| Глобальная область видимости |
module, baremodule, в интерактивном режиме (REPL) |
| Локальная область видимости |
Мягкая локальная область видимости: for, while, выражения со списком, try-catch-finally, let
|
| Локальная область видимости |
Жёсткая локальная область видимости: функции (любой синтаксис, анонимные и do-блоки), struct, macro
|
В этой таблице отсутствуют блоки 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.)
Внутри локальной области видимости переменную можно явно сделать локальной, используя ключевое слово local:
julia> x = 0;
julia> for i = 1:10
local x
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.
Мягкие локальные области видимости вводятся циклами for, циклами while, выражениями со списком, блоками try-catch-finally и блоками let. Есть некоторые дополнительные правила для блоков let и для циклов for и выражений со списком.
В следующем примере x и y всегда ссылаются на те же переменные, так как мягкая локальная область видимости наследует переменные для чтения и записи:
julia> x, y = 0, 1;
julia> for i = 1:10
x = i + y + 1
end
julia> x
12
Внутри мягких областей видимости ключевое слово global никогда не требуется, хотя и разрешено. Единственный случай, когда оно изменит семантику, — это (в настоящее время) синтаксическая ошибка:
julia> let
local j = 2
let
global j = 3
end
end
ERROR: syntax: `global j`: j is local variable in the enclosing scope
Жёсткая локальная область видимости
Жёсткие локальные области видимости вводятся определениями функций (во всех их формах), определениями типов struct и определениями макросов.
В жёсткой локальной области видимости все переменные наследуются из родительской области видимости, за исключением следующих случаев:
присваивание привело бы к изменению глобальной переменной, или
переменная явно отмечена ключевым словом
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
(1, 2)
Различие в наследовании глобальных и локальных переменных при присваивании может привести к некоторым небольшим различиям между функциями, определёнными в локальной и глобальной областях видимости. Рассмотрим модификацию последнего примера, перенеся bar в глобальную область видимости:
julia> x, y = 1, 2;
julia> function bar()
x = 10 # local
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
(1, 2)
Обратите внимание, что вышеупомянутая тонкость не относится к определениям типов и макросов, так как они могут появляться только в глобальной области видимости. Есть специальные правила области видимости, касающиеся оценки аргументов функций по умолчанию и ключевых аргументов, которые описаны в разделе Функций.
Присваивание, вводящее переменную, используемую внутри определения функции, типа или макроса, не должно предшествовать её внутреннему использованию:
julia> f = y -> y + a (::#1) (generic function with 1 method) julia> f(3) ERROR: UndefVarError: a not defined Stacktrace: [1] (::##1#2)(::Int64) at ./none:1 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(), поэтому вышеприведённые определения следует рассматривать только как примеры.
Жёсткая против мягкой локальной области видимости
Блоки, которые вводят мягкую локальную область видимости, такие как циклы, обычно используются для манипулирования переменными в родительской области видимости. Поэтому их значением по умолчанию является полный доступ ко всем переменным в родительской области видимости.
Напротив, код внутри блоков, которые вводят жёсткую локальную область видимости (определения функций, типов и макросов), может выполняться в любом месте программы. Дистанционное изменение состояния глобальных переменных в других модулях следует производить с осторожностью, и поэтому это функция, которая включается по запросу, требующая ключевого слова global.
Причина, по которой разрешено изменять локальные переменные родительских областей видимости во вложенных функциях, заключается в возможности создания замыканий с закрытым состоянием, например, переменная state в следующем примере:
julia> let
state = 0
global counter
counter() = state += 1
end;
julia> counter()
1
julia> counter()
2
См. также примеры замыканий в следующих двух разделах.
Блоки 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 = Array{Any}(2); i = 1;
julia> while i <= 2
Fs[i] = ()->i
i += 1
end
julia> Fs[1]()
3
julia> Fs[2]()
3
Здесь мы создаём и сохраняем два замыкания, которые возвращают переменную i. Однако это всегда та же самая переменная i, поэтому два замыкания ведут себя одинаково. Мы можем использовать let для создания новой привязки для i:
julia> Fs = Array{Any}(2); i = 1;
julia> while i <= 2
let i = i
Fs[i] = ()->i
end
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 циклов, которые повторно используют переменные для всех итерационных шагов. Поэтому эти конструкции похожи на while циклы с блоками let внутри:
julia> Fs = Array{Any}(2);
julia> for j = 1:2
Fs[j] = ()->j
end
julia> Fs[1]()
1
julia> Fs[2]()
2
for циклы будут повторно использовать существующие переменные для переменной итерации:
julia> i = 0;
julia> for i = 1:3
end
julia> i
3
Однако, выражения-генераторы этого не делают и всегда создают новые переменные итерации:
julia> x = 0; julia> [ x for x = 1:3 ]; julia> x 0
Константы
Распространенное использование переменных — присвоение им имен для конкретных неизменяемых значений. Такие переменные присваиваются только один раз. Это намерение может быть сообщено компилятору с помощью ключевого слова const.
julia> const e = 2.71828182845904523536; julia> const pi = 3.14159265358979323846;
Объявление const разрешено как для глобальных, так и для локальных переменных, но особенно полезно для глобальных. Компилятору сложно оптимизировать код, включающий глобальные переменные, так как их значения (или даже их типы) могут изменяться практически в любое время. Если глобальная переменная не будет изменяться, добавление объявления const решает эту проблему производительности.
Локальные константы сильно отличаются. Компилятор может автоматически определить, когда локальная переменная является константой, поэтому объявления локальных констант не нужны для повышения производительности.
Специальные присваивания на верхнем уровне, такие как те, которые выполняются с помощью ключевых слов function и struct, по умолчанию являются константами.
Обратите внимание, что const влияет только на привязку переменной; переменная может быть привязана к изменяемому объекту (такому как массив), и этот объект все равно может быть изменён.
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.6/manual/variables-and-scoping/