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

Область видимости ( англ. scope ) в программировании — часть программы , в пределах которой идентификатор , объявленный как имя некоторой программной сущности (обычно — переменной , типа данных или функции ), остаётся связанным с этой сущностью, то есть позволяет посредством себя обратиться к ней. Говорят, что идентификатор объекта «виден» в определённом месте программы, если в данном месте по нему можно обратиться к данному объекту. За пределами области видимости тот же самый идентификатор может быть связан с другой переменной или функцией, либо быть свободным (не связанным ни с какой из них). Область видимости может, но не обязана совпадать с областью существования объекта, с которым связано имя.

Cвязывание идентификатора ( англ. binding ) в терминологии некоторых языков программирования — процесс определения программного объекта, доступ к которому даёт идентификатор в конкретном месте программы и в конкретный момент её выполнения. Это понятие по сути синонимично области видимости , но может быть более удобно при рассмотрении некоторых аспектов выполнения программ.

Области видимости входят друг в друга и составляют иерархию , от локальной области видимости, ограниченную функцией (или даже её частью), до глобальной, идентификаторы которой доступны во всей программе. Также в зависимости от правил конкретного языка программирования области видимости могут быть реализованы двумя способами: лексически (статически) или динамически .

Область видимости также может иметь смысл для языков разметки : например, в HTML областью видимости имени элемента управления является форма (HTML) от <form> до </form> .

Типы области видимости

В монолитной (одномодульной) программе без вложенных функций и без использования ООП может существовать только два типа области видимости: глобальная и локальная. Прочие типы существуют только при наличии в языке определённых синтаксических механизмов.

  • Глобальная область видимости — идентификатор доступен во всём тексте программы (во многих языках действует ограничение — только в тексте, находящемся после объявления этого идентификатора).
  • Локальная область видимости — идентификатор доступен только внутри определённой функции (процедуры).
  • Видимость в пределах модуля может существовать в модульных программах , состоящих из нескольких отдельных фрагментов кода, обычно находящихся в разных файлах. Идентификатор, чьей областью видимости является модуль, доступен из любого кода в пределах данного модуля.
  • Пакет или пространство имён . В глобальной области видимости искусственно выделяется поименованная подобласть. Имя «привязывается» к этой части программы и существует только внутри неё. Вне данной области имя либо вообще недоступно, либо доступно ограниченно.

В ООП -языках дополнительно к вышеперечисленным могут поддерживаться специальные ограничения области видимости, действующие только для членов классов (идентификаторов, объявленных внутри класса или относящихся к нему):

  • Приватная (личная, закрытая) ( англ. private ) область видимости означает, что имя доступно только внутри методов своего класса .
  • Защищённая ( англ. protected ) область видимости означает, что имя доступно только внутри своего класса и его классов-потомков.
  • Общая ( англ. public ) область видимости означает, что имя доступно в пределах области видимости, к которой относится его класс.

Способы задания области видимости

В простейших случаях область видимости определяется местом объявления идентификатора. В случаях, когда место объявления не может однозначно задать область видимости, применяются специальные уточнения.

  • Идентификатор, объявленный вне любого определения функции, процедуры, типа, является глобальным.
  • Идентификатор, объявленный внутри определения функции, является локальным в данной функции, то есть его областью видимости является эта функция.
  • Идентификатор, являющийся частью определения типа данных, в отсутствие дополнительных уточнений имеет ту же область видимости, что и идентификатор типа, в определение которого он входит.
  • В языках, поддерживающих модули, пакеты или пространства имён идентификатор, объявленный вне всех процедур и классов, по умолчанию относится к модулю, пакету или пространству имён, внутри которого находится его объявление. Сами пределы области видимости для пакета или пространства имён указываются с помощью специальных описаний, а модульная область видимости ограничивается обычно текущим файлом исходного текста программы. Особенностью этого типа видимости является то, что язык, как правило, содержит средства, позволяющие сделать идентификатор доступным и вне своего модуля (пакета или пространства имён), то есть «расширить» его область видимости. Для этого должно иметься сочетание двух факторов: содержащий идентификатор модуль должен быть импортирован с помощью специальной команды там, где предполагается его использование, а сам идентификатор при его описании должен быть дополнительно объявлен экспортируемым . Способы объявления идентификатора экспортируемым могут быть различны. Это могут быть специальные команды или модификаторы в описаниях, соглашения об именовании (например, в языке Go экспортируемыми являются идентификаторы пакетной области видимости, начинающиеся на заглавную букву). В ряде языков каждый модуль (пакет) искусственно делится на две части: раздел определений и раздел реализации, которые могут находиться как в пределах одного файла исходного кода (например, в Delphi), так и в разных (например, в языке Модула-2 ); экспортируемыми являются идентификаторы, объявленные в модуле определений.
  • Область видимости идентификатора, объявленного внутри ООП-класса, по умолчанию является либо приватной, либо общей. Иная область видимости придаётся с помощью специального описания (например, в C++ это модификаторы 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 .

Лексические 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. от 4 декабря 2012 на Wayback Machine , переводчик: А. Пирамидин, intuit.ru, ISBN 978-5-94774-648-8 , 17. Лекция: Формы.
  2. . Дата обращения: 11 марта 2013. 26 ноября 2019 года.
Источник —

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