Область видимости

Область видимости (англ. 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
       }
    }
}

Примечания

  1. Спецификация языка HTML Архивная копия от 4 декабря 2012 на Wayback Machine, переводчик: А. Пирамидин, intuit.ru, ISBN 978-5-94774-648-8, 17. Лекция: Формы.
  2. Области видимости. Дата обращения: 11 марта 2013. Архивировано 26 ноября 2019 года.