Spec-Zone.ru › Julia 0.7

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

Область видимости переменной — это область кода, в которой переменная видна. Область видимости переменных помогает избежать конфликтов имён переменных. Концепция интуитивна: две функции могут иметь аргументы, называемые x, без того, чтобы два x ссылались на одно и то же. Аналогично, существуют многочисленные случаи, когда разные блоки кода могут использовать одно и то же имя, не ссылаясь при этом на одну и ту же сущность. Правила, определяющие, ссылаются ли одинаковые имена переменных на одну и ту же сущность, или нет, называются правилами области видимости; в этом разделе они подробно описаны.

Определенные конструкции языка вводят блоки области видимости, которые представляют собой области кода, потенциально являющиеся областью видимости набора переменных. Область видимости переменной не может быть произвольным набором строк исходного кода; вместо этого она всегда соответствует одному из этих блоков. В Julia существуют два основных типа областей видимости: глобальная область видимости и локальная область видимости, последняя может быть вложенной. Конструкции, вводящие блоки области видимости, включают:

  • Блоки области видимости, которые могут быть вложены только в другие блоки глобальной области видимости:

    • глобальная область видимости

      • модуль, baremodule

      • в интерактивном режиме (REPL)

    • локальная область видимости (не допускает вложения)

      • (mutable) struct, макрос
  • Блоки области видимости, которые могут быть вложены где угодно (в глобальной или локальной области видимости):

    • локальная область видимости

      • for, while, try-catch-finally, let

      • функции (любой синтаксис, анонимные и do-блоки)

      • генераторы, операция broadcast-fusing

В этой таблице отсутствуют блоки 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:

Избегание глобальных переменных

Избегание изменения значений глобальных переменных считается многими программистами лучшей практикой. Одна из причин заключается в том, что удалённое изменение состояния глобальных переменных в других модулях требует осторожности, поскольку это затрудняет понимание локального поведения программы. Именно поэтому блоки области видимости, вводящие локальную область видимости, требуют ключевого слова $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

Обратите внимание, что, хотя это возможно, изменение значения переменной, объявленной как константа, настоятельно не рекомендуется. Например, если метод ссылается на константу и уже скомпилирован до изменения константы, он может продолжать использовать старое значение:

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

Spec-Zone.ru

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