Spec-Zone.ru › Julia 1.2

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

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

Переменная цикла или генератора списков всегда является новой переменной:

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.2.0/manual/variables-and-scoping/

Spec-Zone.ru

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