Область видимости
- 1 year ago
- 0
- 0
Область видимости ( англ. scope) в программировании — часть программы , в пределах которой идентификатор , объявленный как имя некоторой программной сущности (обычно — переменной , типа данных или функции ), остаётся связанным с этой сущностью, то есть позволяет посредством себя обратиться к ней. Говорят, что идентификатор объекта «виден» в определённом месте программы, если в данном месте по нему можно обратиться к данному объекту. За пределами области видимости тот же самый идентификатор может быть связан с другой переменной или функцией, либо быть свободным (не связанным ни с какой из них). Область видимости может, но не обязана совпадать с областью существования объекта, с которым связано имя.
Cвязывание идентификатора ( англ. binding) в терминологии некоторых языков программирования — процесс определения программного объекта, доступ к которому даёт идентификатор в конкретном месте программы и в конкретный момент её выполнения. Это понятие по сути синонимично области видимости , но может быть более удобно при рассмотрении некоторых аспектов выполнения программ.
Области видимости входят друг в друга и составляют иерархию , от локальной области видимости, ограниченную функцией (или даже её частью), до глобальной, идентификаторы которой доступны во всей программе. Также в зависимости от правил конкретного языка программирования области видимости могут быть реализованы двумя способами: лексически (статически) или динамически .
Область видимости также может иметь смысл для языков разметки : например, в HTML областью видимости имени элемента управления является форма (HTML) от <form> до </form> .
В монолитной (одномодульной) программе без вложенных функций и без использования ООП может существовать только два типа области видимости: глобальная и локальная. Прочие типы существуют только при наличии в языке определённых синтаксических механизмов.
В ООП -языках дополнительно к вышеперечисленным могут поддерживаться специальные ограничения области видимости, действующие только для членов классов (идентификаторов, объявленных внутри класса или относящихся к нему):
В простейших случаях область видимости определяется местом объявления идентификатора. В случаях, когда место объявления не может однозначно задать область видимости, применяются специальные уточнения.
private
,
public
,
protected
)
.
Приведённый перечень не исчерпывает всех нюансов определения области видимости, которые могут иметься в конкретном языке программирования. Так, например, возможны различные толкования сочетаний модульной области видимости и объявленной видимости членов ООП-класса. В одних языках (например, C++) объявление личной или защищённой области видимости для члена класса ограничивает доступ к нему из любого кода, не относящегося к методам своего класса. В других (Object Pascal) все члены класса, в том числе личные и защищённые, полностью доступны в пределах того модуля, в котором объявлен класс, а ограничения области видимости действуют только в других модулях, импортирующих данный.
Области видимости в программе естественным образом составляют многоуровневую структуру, в которой одни области входят в состав других. Иерархия областей обычно строится на всех или некоторых уровнях из набора: «глобальная — пакетные — модульные — классов — локальные» (конкретный порядок может несколько отличаться в разных языках).
Пакеты и пространства имён могут иметь несколько уровней вложенности, соответственно, вложенными будут и их области видимости. Отношения областей видимости модулей и классов могут сильно отличаться в разных языках. Локальные пространства имён также могут быть вложенными, причём даже в тех случаях, когда язык не поддерживает вложенные функции и процедуры. Так, например, в языке C++ вложенных функций нет, но каждый составной оператор (содержащий набор команд, заключённый в фигурные скобки) образует собственную локальную область видимости, в которой возможно объявление своих переменных.
Иерархическая структура позволяет разрешать неоднозначности, которые возникают, когда один и тот же идентификатор используется в программе более чем в одном значении. Поиск нужного объекта всегда начинается с той области видимости, в которой располагается обращающийся к идентификатору код. Если в данной области видимости находится объект с нужным идентификатором, то именно он и используется. Если такового нет, транслятор продолжает поиск среди идентификаторов, видимых в объемлющей области видимости, если его нет и там — в следующей по уровню иерархии.
program Example1; var a,b,c: Integer; (* Глобальные переменные. *) procedure f1; var b,c: Integer (* Локальные переменные процедуры f1. *) begin a := 10; (* Изменяет глобальную a. *) b := 20; (* Изменяет локальную b. *) c := 30; (* Изменяет локальную с. *) writeln(' 4: ', a, ',', b, ',', c); end; procedure f2; var b,c: Integer (* Локальные переменные процедуры f2. *) procedure f21; var c: Integer (* Локальная переменная процедуры f21. *) begin a := 1000; (* Изменяет глобальную a. *) b := 2000; (* Изменяет локальную b процедуры f2. *) c := 3000; (* Изменяет локальную c процедуры f21.*) writeln(' 5: ', a, ',', b, ',', c); end; begin a := 100; (* Изменяет глобальную a. *) b := 200; (* Изменяет локальную b. *) c := 300; (* Изменяет локальную c. *) writeln(' 6: ', a, ',', b, ',', c); f21; writeln(' 7: ', a, ',', b, ',', c); end; begin (* Инициализация глобальных переменных. *) a := 1; b := 2; c := 3; writeln(' 1: ', a, ',', b, ',', c); f1; writeln(' 2: ', a, ',', b, ',', c); f2; writeln(' 3: ', a, ',', b, ',', c); end.
Так, при запуске приведённой выше программы на языке Паскаль будет получен следующий вывод:
1: 1,2,3 4: 10,20,30 2: 10,2,3 6: 100,200,300 5: 1000,2000,3000 7: 1000,2000,300 3: 1000,2,3
В функции
f1
переменные
b
и
c
находятся в локальной области видимости, поэтому их изменения не затрагивают одноимённые глобальные переменные. Функция
f21
содержит в своей локальной области видимости только переменную
c
, поэтому она изменяет и глобальную
a
, и
b
, локальную в объемлющей функции
f2
.
Использование локальных переменных — имеющих ограниченную область видимости и существующих лишь внутри текущей функции — помогает избежать конфликта имён между двумя переменными с одинаковыми именами. Однако существует два очень разных подхода к вопросу о том, что значит «быть внутри» функции и, соответственно, два варианта реализации локальной области видимости:
Для «чистых» функций, которые оперируют только своими параметрами и локальными переменными, лексическая и динамическая области видимости всегда совпадают. Проблемы возникают, когда функция использует внешние имена, например, глобальные переменные или локальные переменные функций, в которые она входит или из которых вызывается. Так, если функция
f
вызывает не вложенную в неё функцию
g
, то при лексическом подходе функция
g
не имеет
доступа к локальным переменным функции
f
. При динамическом же подходе функция
g
будет
иметь доступ к локальным переменным функции
f
, поскольку
g
была вызвана во время работы
f
.
Например, рассмотрим следующую программу:
x=1 function g () { echo $x ; x=2 ; } function f () { local x=3 ; g ; } f # выведет 1 или 3? echo $x # выведет 1 или 2?
Функция
g()
выводит и изменяет значение переменной
x
, но эта переменная не является в
g()
ни параметром, ни локальной переменной, то есть она должна быть связана со значением из области видимости, в которую входит
g()
. Если язык, на котором написана программа, использует лексические области видимости, то имя
«x»
внутри
g()
должно быть связано с
глобальной
переменной
x
. Функция
g()
, вызванная из
f()
, выведет первоначальное значение
глобальной
х
, после чего поменяет его, и изменённое значение будет выведено последней строкой программы. То есть программа выведет сначала 1, затем 2. Изменения локальной
x
в тексте функции
f()
на этом выводе никак не отразятся, так как эта переменная не видна ни в глобальной области, ни в функции
g()
.
Если же язык использует динамические области видимости, то имя
«x»
внутри
g()
связывается с
локальной
переменной
x
функции
f()
, поскольку
g()
вызывается изнутри
f()
и
входит
в её область видимости. Здесь функция
g()
выведет локальную переменную
x
функции
f()
и изменит её же, а на значении глобальной x всё это никак не скажется, поэтому программа выведет сначала 3, затем 1. Поскольку в данном случае программа написана на
bash
, который использует динамический подход, в реальности именно так и произойдёт.
И лексическое, и динамическое связывание имеют свои положительные и отрицательные стороны. Практически выбор между тем и другим разработчик делает исходя как из собственных предпочтений, так и из характера проектируемого языка программирования. Большинство типичных императивных языков высокого уровня, изначально рассчитанных на использование компилятора (в код целевой платформы или в байт-код виртуальной машины, не принципиально), реализуют статическую (лексическую) область видимости, так как она удобнее реализуется в компиляторе. Компилятор работает с лексическим контекстом, который статичен и не меняется при исполнении программы, и, обрабатывая обращение к имени, он может легко определить адрес в памяти, где располагается связанный с именем объект. Динамический контекст недоступен компилятору (так как он может меняться в ходе исполнения программы, ведь одна и та же функция может вызываться во множестве мест, причём не всегда явно), так что для обеспечения динамической области видимости компилятор должен добавить в код динамическую поддержку определения объекта, на который ссылается идентификатор. Это возможно, но снижает скорость работы программы, требует дополнительной памяти и усложняет компилятор.
В случае с интерпретируемыми языками (например, скриптовыми ) ситуация принципиально иная. Интерпретатор обрабатывает текст программы непосредственно в момент исполнения и содержит внутренние структуры поддержки исполнения, в том числе таблицы имён переменных и функций с реальными значениями и адресами объектов. Интерпретатору проще и быстрее выполнить динамическое связывание (простым линейным поиском в таблице идентификаторов), чем постоянно отслеживать лексическую область видимости. Поэтому интерпретируемые языки чаще поддерживают динамическое связывание имён.
В рамках как динамического, так и лексического подхода к связыванию имён могут быть нюансы, связанные с особенностями конкретного языка программирования или даже его реализации. В качестве примера рассмотрим два Си-подобных языка программирования: JavaScript и Go . Языки синтаксически довольно близки и оба используют лексическую область видимости, но, тем не менее, различаются деталями её реализации.
В следующем примере показаны два текстуально аналогичных фрагмента кода на JavaScript и Go. В обоих случаях в глобальной области видимости объявляется переменная
scope
, инициализированная строкой «global», а в функции
f()
сначала выполняется вывод значения scope, затем — локальное объявление переменной с тем же именем, инициализированное строкой «local», и, наконец, повторный вывод значения
scope
. Далее приведён реальный результат выполнения функции
f()
в каждом случае.
JavaScript | Go |
---|---|
var scope = "global"; function f() { alert(scope); // ? var scope = "local"; alert(scope); }
|
var scope = "global" func f() { fmt.Println(scope) // ? var scope = "local" fmt.Println(scope) }
|
undefined
local |
global
local |
Легко видеть, что разница заключается в том, какое значение выводится в строке, помеченной комментарием со знаком вопроса.
alert(scope)
локальная переменная scope уже существует и доступна, но ещё не получила значения, то есть, по правилам языка, имеет специальное значение
undefined
. Именно поэтому в помеченной строке будет выведено «undefined».
f()
, но
до объявления
локальной переменной
scope
эта переменная недоступна, и помеченная знаком вопроса команда выводит значение
глобальной
переменной
scope
, то есть «global».
Ещё один нюанс в семантике лексической области видимости — наличие или отсутствие так называемой «блочной видимости», то есть возможности объявить локальную переменную не только внутри функции, процедуры или модуля, но и внутри отдельного блока команд (в Си-подобных языках — заключённого в фигурные скобки
{}
). Далее приведён пример идентичного кода на двух языках, дающего разные результаты выполнения функции
f()
.
JavaScript | Go |
---|---|
function f () { var x = 3; alert(x); for (var i = 10; i < 30; i+=10) { var x = i; alert(x); } alert(x); // ? }
|
func f() { var x = 3 fmt.Println(x) for i := 10; i < 30; i += 10 { var x = i fmt.Println(x) } fmt.Println(x) // ? }
|
3
10 20 20 |
3
10 20 3 |
Разница проявляется в том, какое значение будет выведено последним оператором в функции
f()
, помеченным знаком вопроса в комментарии.
x
значений
i
внутри цикла
for
изменяет единственную локальную переменную
x
, которая была объявлена в начале функции. Поэтому после завершения цикла переменная
x
сохраняет последнее значение, присвоенное ей в цикле. Это значение и выводится в результате.
x
— это новая переменная, областью видимости которой является только тело цикла; она перекрывает
x
, объявленную в начале функции. Эта «дважды локальная» переменная получает в каждом проходе цикла новое значение и выводится, но её изменения не затрагивают объявленную вне цикла переменную
x
. После завершения цикла объявленная в нём переменная
х
прекращает своё существование, а первая
x
становится снова видна. Её значение остаётся прежним, оно и выводится в результате.
Не следует отождествлять видимость идентификатора с существованием значения, с которым данный идентификатор связан. На соотношение видимости имени и существования объекта влияет логика программы и класс памяти объекта. Далее несколько типичных примеров.
// Начинается глобальная область видимости. int countOfUser = 0; int main() { // С этого момента объявляется новая область видимости, в которой видна глобальная. int userNumber[10]; }
#include <stdio.h> int a = 0; // глобальная переменная int main() { printf("%d", a); // будет выведено число 0 { int a = 1; // объявлена локальная переменная а, глобальная переменная a не видна printf("%d", a); // будет выведено число 1 { int a = 2; // еще локальная переменная в блоке, глобальная переменная a не видна, не видна и предыдущая локальная переменная printf("%d", a); // будет выведено число 2 } } }