лекция 17 (1161112), страница 2
Текст из файла (страница 2)
Рассмотрим сначала язык Ада:
А) Определение ИС:
имя ИС: exception // очень похоже на определение переменной name:type, пример X:integer
FILE_ERROR:exception
-
Есть 6 предопределенных типов исключений, например
RANGE_CHECK_ERROR // ошибка диапазона
X(i) = 0; // если i выходит за границы диапазона массива, то генерируется RANGE_CHECK_ERROR
-
Пользователь может вводить свои собственное исключения, произвольное количество.
Считается, что любая программа на Ада погружена внутрь пакета STANDARD, и 6 стандартных исключений описаны внутри этого пакета.
Объявление исключений очень похожи на объявления переменных и подчиняются тем же правилам (типа уникальности имён, область видимости и т.д.). ИС очень похожи на встроенный тип данных. Имя ИС - не имя переменной, а скорее всего имя константы. Нет как таковых операций с константами. Всё, что мы можем сделать с такой константой - это возбудить ИС (raise имя_ИС) и поймать ИС (when имя_ИС => реакция).
Б)Возникновение ИС:
Все условия возникновения стандартных ИС описаны стандартом языка, пользовательских ИС - при помощи raise имя_ИС.
При этом механизм обработки пользовательских ИС и стандартных ИС одинаков, что очень важно.
В)Распространение ИС:
ИС могут возникнут либо в процессе вычисления какого-то выражения, либо с помощью оператора raise, могут возникнуть в каком-то блоке (например, теле процедуры):
procedure P is //тело процедуры
// local definitions
begin
operators
end P;
или
declare // специальный оператор блока внутри процедуры; похоже на {блок} из Си
// local definitions
begin
operators;
end;
Если в блоке была вызвана ИС, то весь этот блок становится аварийным (а не оператор, где она возник). И в конце любого блока могут стоять ловушки, которые будут ловить ИС. Если ловушка этого блока не словила ИС, то весь блок считается завершенным аварийно, идёт возврат в то место, откуда был вызван этот блок, тот блок объявляется аварийным, ищутся ловушки того блока и т.д (рекурсивно – процесс распространения исключения, этот процесс идет динамически)1.
В современных яп мы используем статические области видимости, основанные на вложенности блоков:
begin
I:integer;
begin
j:integer;
end;
end;
Динамические области видимости зависят от динамической вложенности блоков, рассмотим пример:
блок Р {
блок Р1{
блок Р2
}
}
Текстуальная расположенность блоков Р, Р1 и Р2 говорит о статической вложенности. Может быть динамическая вложенность, например, мы вызываем Р, внутри Р вызывается процедура Р2, а внутри Р2 вызывается Р1, которая в свою очередь вызывает Р. Заметим еще раз, что распространение исключений происходит динамически:
P | |
Р2 | 3) придем сюда, ищем ловушки. Если нет ловушек, считаем весь блок ошибочным и идем в Р (верхнее). |
Р1 | 2) придем сюда, ищем ловушки. Если нет ловушек, считаем весь блок ошибочным и идем в Р2. |
Р raise e | 1)Если исключение возникло здесь, то мы считаем весь этот блок ошибочным, и если не нашлось ловушек, то поднимаемся не по тексту программы, а по стеку вызовов. Нас вызвал Р1. |
Г) Обработка ИС:
Общий вид блока:
begin
операторы (генерируют ИС Е)
//идут ловушки
when имя1|имя2|имя3|... => реакция // '|' – либо; реакция – список операторов
when...
when...
when others => реакция
end
Механизм обработки ошибки не зависит от того, где ошибка была порождена, ведь ошибка может вернуться с функцией вызванной из другого блока, ошибка может быть порождена компилятором и т.д. Главное, что ошибка добралась до нашего блока, далее идет сопоставление ошибки с ловушками (сверху вниз, слева направо). Если сопоставление произошло, то вызываем блок реакции (список операторов), и если эти операторы завершились нормально, то ИС обработана, блок завершился нормально, и программа работает дальше, как будто ничего не происходило. Обработка состояла в том, что мы нашли обработчика и выполнили его. Заметим, что when others ловит любую ИС, он должен стоять последним.
При таком подходе мы пишем код обработки ошибки в том месте, где его надо писать (так на примере ошибки считывания cfg-файла: она произойти может в неизвестно где, но где мы будем ее обрабатывать понятно)! Получается у нас текстуально идет код для нормальной ситуации, а потом обработка ошибок. Поэтому обработка исключений становится структурированной.
Если мы перехватили какую-то ИС и не смогли обработать её до конца, например:
begin
OpenFile
P// внутри нее произошла ошибка
CloseFile
when E => обработка // если ошибку из Р мы не можем обработать то переходим на
when others
when others => CloseFile;
raise;
end
Если при чтении файла произошла какая-то ошибка, причём нам неизвестно, что за ошибка, то что делать мы не знаем, но закрыть файл мы должны. Но и ИС мы не обработали, так что надо перевозбудить ИС, заново вызвав raise. Raise без имя_ИС, это перевозбуждение исключительной ситуации, с которой мы пришли в обработчик, кроме того raise без параметра может встречаться только внутри обработчика when.
Возможно 3 случая действий в момент прихода к ловушкам:
1. Обработка ИС.
2. Перевозбуждение ИС.
3. Внутри обработчика мы возбуждаем _другую_ ИС, считая текущую ИС уже обработанной – такое «завертывание» одной ситуации в другую.
Заметим, что существуют 2 принципа распространения и обработки ИС:
1. Динамическая ловушка ("заберите нас и обработайте") - рассмотренный нами принцип распространения и обработки ИС, на примере Ада
2. Ремонт на месте ("сами покопаемся и поедем").
Например, в Вasic:
1) ON операция оператор_обработки // вызов процедуры, либо чаще всего goto handler
2) handler:
resume (возобновить с того же оператор, где произошла ошибка)
resume_next (проигнорировать оператор с ошибкой и пойти дальше)
return (вернуться, выйти из этого блока)
1)Семантика возобновления - семантика бейсика, есть возможность возобновить с места, где произошла ошибка, проигнорировать ошибку.
-
Семантика возобновления иногда очень полезна, рассмотрим пример:
х = new р;
Если памяти под х не хватает, то можно было бы реализовать такой обработчик, который вызывает динамическую сборку мусора, а потом вернуться на х = new р; и места уже хватило бы.
-
Кроме того, самый понятный пример семантики возобновления можно привести для работы с флоппи диском в MS DOS, если выдается сообщение что устройство не найдено, и предлагается сделать:
Abort - return
Retry - resume
Ignore – resume next
-
С помощью семантики возобновления можно промоделировать семантику завершения
2) Семантика завершения - семантика raise, управление на raise никогда обратно не перейдет.
-
Однако для больших реальных проектов требуется семантика завершения, которая хотя и менее гибкая, но повышает производительность. Кроме того, иногда требуется, что бы блок с ошибкой завершился.
-
И можно все-таки привести пример, когда семантика завершения моделирует семантику возобновления, вот для отведения новой памяти на Си++:
for (;;) {
try {
p = get_resource(); //попробуй ресурс, если ресурса не хватает возбуждается исключительная ситуация типа Е
} catch (Е* t) {
free_resource () //освободи ресурс если его нашел; если не нашел перевозбуждаем ИС, чтобы выскочить из цикла
}
}
Во всех современных яп принят принцип семантики завершения (динамической ловушки). Все, что было рассказано о том, как определяются, возникают, распространяются и ловятся – это все применимо ко всем современным яп, где реализован механизм обработки исключений. Есть некие тонкости.
1. В Аде (и некоторых других языках) предполагается, что ИС могут возникнуть в любом месте программы, и программист обязан обрабатывать эти ИС. В Си++ немного другой подход: если пользователю не нужна обработка ИС (не нужна отказоустойчивая программа), то ничего не приформировывается («пользователь не платит за то что не использует»).
2. Принцип Си++ (который распространился и на другие языки – Си#, Java, Delphi): ИС отождествили с типом данных.
Так ИС языка Ада с точки зрения Си++ можно продемонстрировать так, отождествив с int:
ИС = int
0: rang_check_error
1: memory_allocation_error
2: file_error и т.д
В первых компиляторах Си++ фирмы Майкрософт были реализован частный случай исключений - структурированные исключения: каждому виду исключений приписана некая целочисленная константа. Эти исключения обернули стандартным Си++ механизмом исключений.
В современных компиляторах есть специальный механизм, который позволяет заворачивать эти структурированные исключения (идут от аппаратуры) в стандартные Си++-ые исключения.
Что будет, если исключение дойдёт до конца и не найдет ловушки? Это так называемое необработанное исключение и программа завершается с кодом соответствующего исключения.
Замечание на следующую лекцию: в Дельфи, Cи#, Java выделен специальный тип данных под исключения (Exception, TException – потомки Object) и рекомендуется все исключения порождать от этих классов.
1 Небольшое отступление про области видимости – статическую и динамическую. Пример c xBase было 2 объявления переменных: GLOBAL L и LOCAL L // где L некоторая переменная, которую эти операторы добавляли в динамическую область видимости.