лекции (2007) (1160825), страница 10
Текст из файла (страница 10)
Из широко используемых языков независимая трансляция реализована, например, в C++ (из соображений совместимости с C). Если говорить о механизме экспорта/импорта имён (который помогает грамотно описывать КТ), то в C++ он реализован с помощью include-файлов. Более того, в C++ разрешён неявный импорт: если в заголовочном файле M.h подключён файл M1.h ("#include M1.h"), а в файле M1.h подключён M2.h, то имена из M2.h будут импортироваться и в файл M.h. В языках Modula-2, Oberon, Delphi, Java и C#, где трансляция зависимая, неявный импорт имён запрещён.
Вообще говоря, в языках Modula-2, Oberon и Delphi зависимая трансляция обеспечивается достаточно простыми средствами. Поскольку логические модули в этих языках совпадают с единицами компиляции, раздельная трансляция прекрасно совмещается с механизмами управления видимостью. Трансляционная библиотека состоит из модулей определений (DEFINITION MODULE) для Modula-2, имён, помеченных "*" или "*-" для Oberon, и интерфейсов модулей (interface) для Delphi. Соответственно, программная библиотека состоит из модулей реализации (IMPLEMENTATION MODULE) для Modula-2, модулей целиком для Oberon, и реализаций модулей (implementation) для Delphi.
В языке Ada всё несколько сложнее. Здесь физический модуль далеко не всегда совпадает с логическим. Способ разбиения программы на логические модули оставлен на откуп программисту (но, тем не менее, гарантируется, что результат раздельной трансляции (объектный код) не зависит от того, как программа разбита на модули). Для такого управления контекстом используются два вида связывания модулей:
1) Одностороннее связывание - аналог системы "сервис-клиент": "клиентский" модуль знает о "сервисном", а "сервисный" модуль ничего о "клиентском" не знает. Это особенно удобно при использованияя стиля программирования "снизу-вверх". Язык Ada позволяет объявлять одни модули в контексте других (так называемая вложенность модулей), или производить одностороннее связывание с помощью ключевого слова "with":
with M;
package P is
...
end P;
Более того, если после "with список_пакетов_1" указать "use список_пакетов_2", где список_пакетов_2 являедся подмножеством списка_пакетов_1, то все имена из списка_пакетов_1 станут видимы непосредственно.
with M; use M;
package P is
...
end P;
2) Двустороннее связывание - в языке Ada реализовано разделение ЕК (с помощью ключевого слова "separate", которое показывает, что пакет должен транслироваться отдельно):
package P is
...
package P_int is
...
end P_int;
...
end P;
...
package body P is
...
package body P_int is separate;
...
end P;
...
separate (P) package body P_int is
...
end P_int;
В языке Java единицей компиляции является файл. В файле должен существовать класс, который является логическим модулем. В роли физического модуля выступает пакет, который включает в себя несколько файлов. Глобальных переменных и функций в Java нет. Таким образом, в языке реализована иерархическая файловая система; для того, чтобы добавить класс в пакет, используется конструкция "package имя_пакета.имя_класса". Для того, чтобы импортировать класс из пакета, используется ключевое слово "import"; например, "import p1.p2.p3.с". Если же необходимо импортировать не какой-то класс, а весь пакет, то вместо имени класса в конце ставится "*": например, "import java.lang.*". Естественно, чтобы класс можно было импортировать, он должен быть публичным (помечен как "public"). Трансляционная библиотека в Java также состоит только из public-имён класса.
В языке C# единицей компиляции, опять же, является файл. Для построения иерархии и управления видимостью вводится такое понятие, как пространство имён ("namespace"). Пространства имён могут вкладываться друг в друга:
namespace N
{
class X;
namespace N1
{
class Y;
...
}
...
}
Доступ осуществляется по иерархии, например, "System.Console.Writeline("Hello, World!")". Однако можно импортировать пространство имён (с помощью ключевого слова "using"), и тогда определённую часть имени (в зависимости от того, что было импортировано), можно опускать; например:
using System;
...
Console.Writeline("Hello, World!");
Глава 8. Обработка исключительных ситуаций (ИС). Понятие исключительной ситуации.
Понятие исключения возникло в языках программирования достаточно давно. Уже в PL/1 были так называемые ON-ситуации:
ON ситуация оператор
То есть в базисе языка был заложен определённый набор ситуаций (например,в PL/1 среди прочих были ситуации OVERFLOW, ENDFILE, ERROR), при возникновении которых сначала выполнялся некоторый оператор, а потом программа либо останавливалась, либо возобновляла выполнение после точки вызова этого оператора. Таким образом, возникновение исключительной ситуации вовсе не означало возникновение ошибки в программе, ведь, например, ситуация ENDFILE (конец файла) вполне нормальна. Поэтому исключительная ситуация рассматривалась, вообще говоря, как событие, которое предусмотрено и которое должно обрабатываться особым образом. В настоящее время под исключительными ситуациями, как правило, понимаются именно "аварии", то есть нежелательные ситуации (конечно, можно использовать этот механизм и для каких-то иных целей, но это считается плохим стилем программирования).
Принято рассматривать два подхода к исключительным ситуациям:
1) Подход, который использовался раньше - так называемый "ремонт на месте" (аналогия с поломкой машины - если возможно, отремонтировать и поехать дальше). Этот подход подразумевает семантику возобновления. Под обработчиком понимается часть кода, которая призвана исправить ошибку. Обработчик действует по одной из трёх схем:
- resume (попытка выполнить тот же самый оператор)
- resume next (игнорирование ошибки и выполнение следующего оператора)
- return (если ничего больше не удаётся сделать, остаётся только прекратить работу программы)
Из современных языков этот подход реализован в Visual Basic (вместо "resume - resume next - return" используется терминология "retry - resume - abort").
2) Современный подход - "принцип динамической ловушки" (аналогия с поломкой машины - поехать дальше уже точно не получится, поэтому необходимо вызвать эвакуатор и ждать, пока машину отвезут в сервис). Соответственно, такой подход подразумевает семантику завершения. Впервые он появился в языке Ada, из него перешёл в C++, а затем и в другие современные языки (C#, Java, Delphi).
Например, в C++ этот подход реализован с помощью конструкции "try - catch". В try-блоке может произойти ошибка; если она произошла, то выполнение программы остнавливается и начинается процесс распространения исключения. Если исключение так и не будет поймано ни в один из catch-блоков, то программа аварийно завершится. Пример неправильного использования этого подхода - попытка сбора мусора при создании динамических объектов ("new - delete"):
try
{
... //попытка выделить память
}
catch(...)
{
... //попытка собрать мусор
continue;
}
Эта проблема имеет семантику возобновления, а не завершения.
семантика завершения, вообще говоря, "тяжелее" семантики возобновления и во многих случаях невыгодна. Однако в крупных проектах надёжность всё-таки нужнее эффективности выполнения (в истории было немало тому примеров, взять хотя бы взрыв шаттла Challenger), поэтому от семантики возобновления некоторое время назад отказались.
Ещё один выход - это теория, разработанная Эдсгером Дейкстрой в 70-х годах. Идея состоит в том, что имея строго описанный набор предусловий, цель получить чётко заданный результат и придерживаясь определённой дисциплины программирования, можно построить абсолютно надёжную программу, по ходу её написания математически доказав и её правильность. Основным фактором, из-за которого этот подход не прижился, стало то, что в сложных системах зачастую невозможно чётко описать предусловия и результат, который должна выдавать программа. Кроме того, заказчик, для которого пишется программа, может поменять часть требований к программе, и тогда придётся полностью переписывать её.
При работе с исключительными ситуациями выделяют четыре аспекта (классификация "по Кауфману"):
1) Определение ИС
2) Возникновение ИС
3) Распространение ИС
4) Обработка ИС
Обработка есть в языках Ada, C++, C#, Java, Delphi. Более того, последние три языка очень похожи друг на друга в смысле обработки ИС, так как в этом отношении они все опираются на C++.
В языке Ada введено специальное ключевое слово "exception". Фактически, exception - это специальный встроенный псевдотип данных. Объекты-исключения объявляются как обычные переменные (e : exception), но у них нет инициализации и операций помимо возбуждения и ловушки. Вообще говоря, в Ada существует 5 видов предопределённых исключений. Подразумевается, что исключительная ситуация может возникнуть в любом месте программы; после тех операторов, где она предположительно возникнет, добавляется блок для её обработки:
...
exception
when список_имён_1 => ...
when список_имён_2 => ...
when others => ...
Ключевое слово "others" отвечает за все исключения, не пойманные ни в одну из предыдущих ловушек; после "when others" не должно стоять других "when" (имеется в виду данный блок). Возбуждение исключительной ситуации происходит при использовании оператора "raise" (например, "raise e"; при перевозбуждении исключения, когда оно обработано не до конца и перенаправляется дальше, возможно использование просто "raise" без имени исключения).
В языке C++ исключительная ситуация может быть связана с произвольным типом данных. Это было сделано потому, что идентификатор исключения несёт слишком мало информации. В C++ при возникновении ошибки можно передать всю нужную информацию через объект исключения, и использовать её при обработке. Кроме того, в C++ вполне допускается, что программист не будет использовать исключения, и не хочет "платить" (в смысле производительности) за то, что не использует. Поэтому места, в которых могут появиться исключительные ситуации, необходимо заключать в так называемые try-блоки. "Отлов" и обработка ИС производятся в списке обработчиков, располагающихся после соответствующего try-блока. Для описания обработчиков используется ключевое слово "catch", а сами они бывают трёх видов:
- catch (тип) {...}
- catch (тип имя) {...}
- catch (...) {...}
Вариант с определением локального имени используется тогда, когда необходимо как-то работать с объектом исключения: получить какую-либо дополнительную информацию или изменить сам объект данных. Обработчик "catch (...)" является аналогом "when others" в языке Ada (отвечает за исключения, оставшиеся неотловленными) и точно также должен быть последним в списке обработчиков. Возникновение исключительной ситуации происходит по оператору "throw" (ключевое слово "raise" не было использовано потому, что оно уже было занято стандартными файлами операционной системы UNIX). Также, как и в языке Ada, для возникновения ИС необходимо писать "throw e" или "throw new e", при перевозбуждении исключения допустимо просто "throw". Кроме того, ключевое слово "throw" вместе со списком возможных возбуждаемых исключений можно добавлять в конец профилей методов: "void f () throw (X, Y, Z);". Если будет возбуждено исключение, которого нет в списке, вызовется стандартная процедура unexpected (); если возбуждённое исключение, выйдя на верхний уровень программы, так и не будет отловлено, вызовется стандартная процедура terminate ().
В языках Java, Delphi и C# действует подход, аналогичный языку C++, только типы данных исключений там не произвольные, а определены в специальных классах (класс "Exception" для языков C# и Delphi, "Throwable" для Java).