Область видимости
Область видимости (англ. scope) в программировании — часть программы, в пределах которой идентификатор, объявленный как имя некоторой программной сущности (обычно — переменной, типа данных или функции), остаётся связанным с этой сущностью, то есть позволяет посредством себя обратиться к ней. Говорят, что идентификатор объекта «виден» в определённом месте программы, если в данном месте по нему можно обратиться к данному объекту. За пределами области видимости тот же самый идентификатор может быть связан с другой переменной или функцией, либо быть свободным (не связанным ни с какой из них). Область видимости может, но не обязана совпадать с областью существования объекта, с которым связано имя.
Cвязывание идентификатора (англ. binding) в терминологии некоторых языков программирования — процесс определения программного объекта, доступ к которому даёт идентификатор в конкретном месте программы и в конкретный момент её выполнения. Это понятие по сути синонимично области видимости, но может быть более удобно при рассмотрении некоторых аспектов выполнения программ.
Области видимости входят друг в друга и составляют иерархию, от локальной области видимости, ограниченную функцией (или даже её частью), до глобальной, идентификаторы которой доступны во всей программе. Также в зависимости от правил конкретного языка программирования области видимости могут быть реализованы двумя способами: лексически (статически) или динамически.
Область видимости также может иметь смысл для языков разметки: например, в HTML областью видимости имени элемента управления является форма (HTML) от <form> до </form>[1].
Типы области видимости
В монолитной (одномодульной) программе без вложенных функций и без использования ООП может существовать только два типа области видимости: глобальная и локальная. Прочие типы существуют только при наличии в языке определённых синтаксических механизмов.
- Глобальная область видимости — идентификатор доступен во всём тексте программы (во многих языках действует ограничение — только в тексте, находящемся после объявления этого идентификатора).
- Локальная область видимости — идентификатор доступен только внутри определённой функции (процедуры).
- Видимость в пределах модуля может существовать в модульных программах, состоящих из нескольких отдельных фрагментов кода, обычно находящихся в разных файлах. Идентификатор, чьей областью видимости является модуль, доступен из любого кода в пределах данного модуля.
- Пакет или пространство имён. В глобальной области видимости искусственно выделяется поименованная подобласть. Имя «привязывается» к этой части программы и существует только внутри неё. Вне данной области имя либо вообще недоступно, либо доступно ограниченно.
В ООП-языках дополнительно к вышеперечисленным могут поддерживаться специальные ограничения области видимости, действующие только для членов классов (идентификаторов, объявленных внутри класса или относящихся к нему):
- Приватная (личная, закрытая) (англ. private) область видимости означает, что имя доступно только внутри методов своего класса.
- Защищённая (англ. protected) область видимости означает, что имя доступно только внутри своего класса и его классов-потомков.
- Общая (англ. public) область видимости означает, что имя доступно в пределах области видимости, к которой относится его класс.
Способы задания области видимости
В простейших случаях область видимости определяется местом объявления идентификатора. В случаях, когда место объявления не может однозначно задать область видимости, применяются специальные уточнения.
- Идентификатор, объявленный вне любого определения функции, процедуры, типа, является глобальным.
- Идентификатор, объявленный внутри определения функции, является локальным в данной функции, то есть его областью видимости является эта функция.
- Идентификатор, являющийся частью определения типа данных, в отсутствие дополнительных уточнений имеет ту же область видимости, что и идентификатор типа, в определение которого он входит.
- В языках, поддерживающих модули, пакеты или пространства имён идентификатор, объявленный вне всех процедур и классов, по умолчанию относится к модулю, пакету или пространству имён, внутри которого находится его объявление. Сами пределы области видимости для пакета или пространства имён указываются с помощью специальных описаний, а модульная область видимости ограничивается обычно текущим файлом исходного текста программы. Особенностью этого типа видимости является то, что язык, как правило, содержит средства, позволяющие сделать идентификатор доступным и вне своего модуля (пакета или пространства имён), то есть «расширить» его область видимости. Для этого должно иметься сочетание двух факторов: содержащий идентификатор модуль должен быть импортирован с помощью специальной команды там, где предполагается его использование, а сам идентификатор при его описании должен быть дополнительно объявлен экспортируемым. Способы объявления идентификатора экспортируемым могут быть различны. Это могут быть специальные команды или модификаторы в описаниях, соглашения об именовании (например, в языке Go экспортируемыми являются идентификаторы пакетной области видимости, начинающиеся на заглавную букву). В ряде языков каждый модуль (пакет) искусственно делится на две части: раздел определений и раздел реализации, которые могут находиться как в пределах одного файла исходного кода (например, в Delphi), так и в разных (например, в языке Модула-2); экспортируемыми являются идентификаторы, объявленные в модуле определений.
- Область видимости идентификатора, объявленного внутри ООП-класса, по умолчанию является либо приватной, либо общей. Иная область видимости придаётся с помощью специального описания (например, в C++ это модификаторы
private
,public
,protected
)[2].
Приведённый перечень не исчерпывает всех нюансов определения области видимости, которые могут иметься в конкретном языке программирования. Так, например, возможны различные толкования сочетаний модульной области видимости и объявленной видимости членов ООП-класса. В одних языках (например, 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
.
Лексические vs. динамические области видимости
Использование локальных переменных — имеющих ограниченную область видимости и существующих лишь внутри текущей функции — помогает избежать конфликта имён между двумя переменными с одинаковыми именами. Однако существует два очень разных подхода к вопросу о том, что значит «быть внутри» функции и, соответственно, два варианта реализации локальной области видимости:
- лексическая область видимости, или лексический контекст (англ. lexical scope), или лексическое (статическое) связывание (англ. lexical (static) binding): локальная область видимости функции ограничена текстом определения этой функции (имя переменной имеет значение внутри тела функции и считается неопределённым за его пределами).
- динамическая область видимости, или динамический контекст (англ. dynamic scope), или динамическое связывание (англ. dynamic binding): локальная область видимости ограничена временем исполнения функции (имя доступно, пока функция выполняется, и исчезает, когда функция возвращает управление вызвавшему её коду).
Для «чистых» функций, которые оперируют только своими параметрами и локальными переменными, лексическая и динамическая области видимости всегда совпадают. Проблемы возникают, когда функция использует внешние имена, например, глобальные переменные или локальные переменные функций, в которые она входит или из которых вызывается. Так, если функция 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 |
Легко видеть, что разница заключается в том, какое значение выводится в строке, помеченной комментарием со знаком вопроса.
- В JavaScript областью видимости локальной переменной является вся функция, в том числе та её часть, которая находится до объявления; при этом инициализация этой переменной выполняется только в момент обработки строки, где она находится. На момент первого вызова
alert(scope)
локальная переменная scope уже существует и доступна, но ещё не получила значения, то есть, по правилам языка, имеет специальное значениеundefined
. Именно поэтому в помеченной строке будет выведено «undefined». - В Go используется более традиционный для этого типа языков подход, согласно которому область видимости имени начинается со строки, где оно объявляется. Поэтому внутри функции
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()
, помеченным знаком вопроса в комментарии.
- В JavaScript не было блочной области видимости(до введения в использование стандарта ES6), а повторное объявление локальной переменной работает просто как обычное присваивание. Присваивание
x
значенийi
внутри циклаfor
изменяет единственную локальную переменнуюx
, которая была объявлена в начале функции. Поэтому после завершения цикла переменнаяx
сохраняет последнее значение, присвоенное ей в цикле. Это значение и выводится в результате. - В Go блок операторов образует локальную область видимости, и объявляемая внутри цикла переменная
x
— это новая переменная, областью видимости которой является только тело цикла; она перекрываетx
, объявленную в начале функции. Эта «дважды локальная» переменная получает в каждом проходе цикла новое значение и выводится, но её изменения не затрагивают объявленную вне цикла переменнуюx
. После завершения цикла объявленная в нём переменнаях
прекращает своё существование, а перваяx
становится снова видна. Её значение остаётся прежним, оно и выводится в результате.
Видимость и существование объектов
Не следует отождествлять видимость идентификатора с существованием значения, с которым данный идентификатор связан. На соотношение видимости имени и существования объекта влияет логика программы и класс памяти объекта. Далее несколько типичных примеров.
- Для переменных, память под которые выделяется и освобождается динамически (в куче), возможно любое соотношение видимости и существования. Переменная может быть объявлена и затем инициализирована, тогда объект, соответствующий имени, фактически появится позже вхождения в область видимости. Но объект может быть создан заранее, сохранён и затем присвоен переменной, то есть появиться раньше. То же и с удалением: после вызова команды удаления для переменной, связанной с динамическим объектом, сама переменная остаётся видимой, но её значение не существует, а обращение к нему приведёт к непредсказуемым результатам. С другой стороны, если команда удаления не вызвана, то объект в динамической памяти может продолжать существовать и после того, как ссылающаяся на него переменная вышла из области видимости.
- Для локальных переменных со статическим классом памяти (в языках Си и C++) значение появляется (логически) в момент запуска программы. При этом имя находится в области видимости только при исполнении содержащей его функции. Причём в промежутках между функциями значение сохраняется.
- Автоматические (в терминологии Си) переменные, создаваемые при входе в функцию и уничтожаемые при выходе, существуют в период времени, когда их имя видно. То есть для них времена доступности и существования практически можно считать совпадающими.
Примеры
Си
// Начинается глобальная область видимости.
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
}
}
}
Примечания
- Спецификация языка HTML, переводчик: А. Пирамидин, intuit.ru, ISBN 978-5-94774-648-8, 17. Лекция: Формы.
- Области видимости . Дата обращения: 11 марта 2013.