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