
3.8. Область видимости
Важной характеристикой переменных является область видимости – совокупность операторов, в которых переменная доступна.
Правила обзора данных в языке определяют, как появление имени связано с переменной. В частности, каким образом ссылки на переменные, объявленные вне выполняющейся в данный момент подпрограммы или блока, связаны с их объявлениями и, вследствие этого, с их атрибутами. Таким образом, для написания или чтения программ на данном языке необходимо полное знание этих правил.
Переменная является локальной в программной единице или блоке, если она там объявлена. В данном разделе программными единицами считаются главный программный модуль или подпрограммы. Единицы, подобные классам языков C++, Java, C# здесь не рассмотрены. Нелокальными переменными программной единицы или блока называются переменные, которые видимы в этой программной единице или блоке, но не объявляются в них.
3.8.1. Статическая область видимости
В языке ALGOL 60 был введен метод связывания имен с нелокальными переменными, названный статическим обзором данных. Позже этот метод был позаимствован большинством императивных, а также и многими неимперативными языками. Использование статического обзора данных получило свое название из-за возможности статического (то есть до периода выполнения) определения области видимости любой переменной.
Большинство отдельных статических областей видимости в императивных языках связаны с определениями программных единиц. Предположим, что все области видимости связаны с программными единицами. В данном разделе будем полагать, что для обращения к нелокальным переменным в обсуждаемых языках используются только области видимости. Последнее не совсем справедливо даже для языков со статическим обзором данных, но такое предположение упрощает обсуждение.
В концепциях многих языков подпрограммы создают собственные области видимости. Во многих распространенных языках со статическим обзором данных, за исключением C-подобных языков и FORTRAN, подпрограммы могут вкладываться в другие подпрограммы, что создает в программе иерархию областей видимости.
Когда в языке со статическим обзором данных компилятор обнаруживает переменную, ее атрибуты определяются путем поиска объявившего ее оператора. При наличии вложенных подпрограмм этот процесс протекает следующим образом. Предположим, что сделано обращение к переменной x подпрограммы sub1. Соответствующее объявление вначале разыскивается в объявлениях подпрограммы sub1. Если для данной переменной объявления не найдено, то поиск продолжается в объявлениях подпрограммы, объявившей подпрограмму subl, называемой статическим родителем подпрограммы sub1. Если объявление переменной x не найдено и в этой подпрограмме, то поиск продолжается в следующей внешней единице (в модуле, объявившем родителя подпрограммы sub1) и так далее, пока не будет найдено объявление переменной x, или поиск в самом внешнем блоке не увенчается успехом. В последнем случае будет зафиксирована ошибка необъявленной переменной. Статический родитель подпрограммы sub1, его статический родитель и так далее вплоть до основной программы называются статическими предками подпрограммы sub1. Отметим, что практические методы реализации статического обзора данных значительно эффективнее, чем описанный здесь процесс.
Рассмотрим следующую процедуру языка Pascal:
procedure big;
var x : integer;
procedure subl;
begin { subl }
... x ...
end; { subl }
procedure sub2;
var x : integer;
begin { sub2 }
…x…
end; { sub2 }
begin { big }
…
end; { big }
При статическом обзоре данных ссылка на переменную x подпрограммы sub1 относится к переменной x, объявленной в процедуре big. Поиск объявления переменной x начался в той процедуре, в которой встречается ссылка (sub1), но объявления этой переменной в ней найдено не было. Далее поиск продолжился в статическом родителе подпрограммы sub1 (подпрограмме big), в котором и было найдено объявление переменной х.
Этот процесс может затруднить наличие предопределенных имен. Иногда предопределенное имя подобно ключевому слову и может переопределяться пользователем. В таких случаях предопределенное имя используется, только если пользовательская программа не содержит переопределения. В других случаях предопределенное имя может быть зарезервированным, что означает начало поиска значения данного имени в списке предопределенных имен, который выполняется даже до проверки объявлений локальной области видимости.
В языках со статическим обзором данных объявления переменных могут быть скрыты от некоторых подпрограмм. В предыдущем примере ссылка на переменную x в процедуре sub2 относится к объявившей переменную x процедуре sub2. В этом случае переменная x программы big скрыта от команд процедуры sub2. Вообще, объявление переменной эффективно скрывает любое объявление одноименной переменной, содержащейся во внешней области видимости.
В языке Ada к переменным, скрытым от областей видимостей предков, можно получить доступ с помощью селективных ссылок, содержащих имя области видимости предка. В предыдущей программе к переменной x процедуры sub2 можно было бы обратиться с помощью ссылки big.x.
Несмотря на то, что в языках C и C++ не разрешено использование вложенных подпрограмм, в этих языках есть глобальные переменные. Эти переменные объявляются вне определения любой функции. Как и в языке Pascal, локальные переменные могут скрывать эти глобальные переменные. В языке C++ к таким скрытым глобальным переменным можно обращаться с помощью операции доступа (::). Например, если переменная x является глобальной переменной, скрытой в подпрограмме локальной переменной x, то обратиться к глобальной переменной можно в форме ::х.
Блоки
Многие языки позволяют создавать новые статические области видимости во время выполнения программы. Эта мощная концепция, впервые появившаяся в языке ALGOL 60, позволяет фрагменту программы иметь собственные локальные переменные с минимизированной областью видимости. Такие переменные, как правило, являются автоматическими, так что память выделяется им в начале выполнения фрагмента программы, а освобождается по окончании его выполнения. Подобный фрагмент программного кода получил название блока.
В языке Ada блоки задаются оператором declare:
declare TEMP : integer;
begin
TEMP := FIRST; FIRST := SECOND; SECOND := TEMP;
end;
От термина «блок» произошло выражение язык с блочной структурой. Хотя языки Pascal и Modula-2 называются языками с блочной структурой, они не имеют непроцедурных блоков.
В языках C, С++, Java, C# любой составной оператор (последовательность операторов, заключенная в фигурные скобки) может содержать объявления и таким образом определять новую область видимости. Например, если list – массив целых чисел, то можно написать следующий код:
if (list [i] < list [j])
{
int temp = list [i];
1ist [j] = temp;
}
Создаваемые блоками области видимости трактуются точно так же, как области видимости, создаваемые подпрограммами. Обращения к переменным блока, не объявляемым в этом блоке, связываются с их объявлениями путем поиска по возрастающей во внешних областях.
В языках C++, Java, C# определять переменные можно в любом месте блока. Если определение появляется не в начале, то область видимости данной переменной начинается с оператора определения и заканчивается концом блока.
Оператор for языков C++, Java, C# позволяет определять переменные в выражениях, инициализирующих счетчики цикла. Область видимости таких переменных ограничена заголовком и телом цикла for.
Определения класса и метода в объектно-ориентированных языках программирования также порождают вложенные статические области видимости.
Применение статических областей видимости представляет метод нелокального доступа, хорошо работающий во многих ситуациях. Однако у этого метода есть и недостатки.
Для простоты будем считать, что все области видимости создаются определениями основной программы и процедур. Структуру программы можно представить в виде дерева, каждый узел которого представляет область видимости, а его родительский узел – родительскую область видимости, в которую вложена данная. Исследование такого дерева показывает, что разрешено значительно большее количество вызовов процедур, чем это необходимо для решения задачи. Программист может по ошибке вызвать подпрограмму, вызов которой не должен допускаться, причем это действие не будет расценено компилятором как ошибка. В результате ошибка будет обнаружена только при выполнении программы, что может повысить стоимость ее исправления. Следовательно, доступ к процедурам должен быть ограничен лишь необходимыми процедурами
С этой проблемой связана возможность слишком интенсивного обращения к данным. Например, все переменные, объявленные в основной программе, видимы во всех вложенных процедурах.
Для решения проблем, связанных со статическим обзором данных, во многих новых языках программирования используется концепция инкапсуляции, то есть возможности сокрытия данных и процедур (фактически – сужения областей видимости).