Исключительные ситуации

Источник: codingrus
Kest

При работе программы могут возникать различного рода ошибки: переполнение, деление на нуль, попытка открыть несуществующий файл и т.п. При возникновении таких исключительных ситуаций программа генерирует прерывание, называемое исключением. В результате выполнение дальнейших вычислений прекращается. Исключение - это объект специального вида, характеризующий возникшую в программе особую ситуацию. Он может также содержать в виде параметров некоторую уточняющую информацию. Особенностью исключений является то, что это сугубо временные объекты. Как только они обработаны каким-то обработчиком, они разрушаются.
Если исключение не перехвачено нигде в программе, то оно автоматически обрабатывается методом TApplication.HandleException. Он обеспечивает стандартную реакцию программы на большинство исключений - выдачу пользователю краткой информации в окне сообщений и уничтожение экземпляра исключения. На рис. 3 приведен пример такого стандартного сообщения для случая целочисленного деления на нуль.
целочисленное деления на нуль
Рис. 3
Если работа по отладке программы ведется в среде Delphi, то при исключениях, подобных указанному на рис. 3, могут появляться сообщения отладчика Delphi, вид которых приведен на рис.4. 
Сообщения исключительных ситуаций
Рис. 4
При этом на экране открывается окно Редактора Кода на строке, при выполнении которой произошло исключение. Необходимо щелкнуть на ОК, а затем следует просмотреть переменные и выяснить причину исключения. После этого возможны следующие действия:
1) нажатие - прекращение выполнения приложения; 
2) нажатие или выполнение команды Run / Run - продолжение работы приложения;
3) нажатие или - продолжение отладки по шагам. 
При необходимости можно отключить появление сообщений отладчика. Для этого надо выполнить команду Tools / Debugger Options, в открывшемся диалоговом окне выбрать страницу Language Exceptions и на ней выключить опцию Stop On Delphi Exceptions. 
Если не принять соответствующих мер в случае генерации исключений, то к неприятностям прекращения вычислений могут добавиться еще неприятности, связанные с так называемой утечкой ресурсов. Под этим подразумеваются потери динамически распределяемой памяти, незакрытые файлы, не уничтоженные временные файлы на диске и прочий информационный "мусор". Например, пусть выполняется некоторая программа, в которой имеются следующие операторы:

AssignFile(F,'a.tmp');
Rewrite (F); 
New(P);

Erase(F); 
Dispose (P);



В программе открывается временный файл с именем a.tmp, чтобы хранить в нем результаты промежуточных вычислений. В конце программы файл должен быть уничтожен процедурой Erase. Кроме того, процедурой New динамически выделяется некоторый объем оперативной памяти. Память будет освобождаться после ее использования процедурой Dispose. Если в промежуточных операторах, помеченных выше точками, возникнет исключение, то вычисления прервутся и процедуры Erase и Dispose не будут выполнены. В результате память, выделенная процедурой New, останется недоступной, а на диске сохранится временный и уже ненужный файл a.tmp.
Избежать подобного рода неприятностей можно несколькими путями:
1. Не допускать исключений, производя предварительную проверку каждой операции, способной привести к исключительным ситуациям.
2. Защищать код очистки информационного "мусора" с помощью программного блока try ... finally.
3. Обрабатывать исключения в блоках try ... except.
Первый из указанных путей борьбы с исключениями предусматривает введение проверки перед выполнением каждой операции, способной привести к исключительным ситуациям. Например, перед делением двух переменных А и В, из которых вторая может оказаться нулем, надо проверить значение В оператором
if ( В = 0 ) then ...



и выполнить необходимые действия, если переменная В окажется равной нулю. Аналогично можно проверять возможность переполнения при делении, умножении и других арифметических операциях, проверять наличие достаточного места в динамически распределяемой памяти при выделении новой области или создании нового объекта, проверять ошибки файлового ввода вывода и т. д. При использовании библиотечных функций можно перед каждым обращением к функции или процедуре производить проверку допустимости всех ее аргументов.
Достоинством рассмотренного подхода к устранению исключений является почти "бессбойная" работа программы (если, конечно, действительно проверяется каждая операция). Слово "бессбойная" здесь недаром поставлено в кавычки. Сложные программы, увы, всегда могут содержать ошибки и возможности сбоя. Даже тщательная предварительная проверка всех операций не способна это устранить. 
При несомненных достоинствах рассмотренного подхода у него есть ряд недостатков. Основной из них - увеличение размера программы (особенно, если она проводит много сложных вычислений) за счет многочисленных операторов проверки и замедление ее работы. К тому же при использовании библиотечных функций и процедур, реализация которых неизвестна, не всегда можно быть уверенным, что проверки гарантируют их бессбойную работу.
В настоящее время проверку отдельных операций и функций на возможность появления исключительных ситуаций надо признать устаревшим подходом, приводящим к неоправданному усложнению программы и снижению ее эффективности. Современный подход заключается в обработке исключений.
Программист должен принять все мыслимые меры, чтобы ни при каких ошибках пользователя и ни при каких сочетаниях данных приложение не заканчивалось бы аварийно. Но если все-таки аварийное завершение происходит, необходима полная очистка "мусора" - удаление временных файлов, освобождение памяти, разрыв связей с базами данных и т. д.
Второй способ борьбы с исключениями состоит в использовании программного блока try ... finally. Блок, содержащий совокупность операторов, способных привести к исключению, можно оформить следующим образом:
try
<операторы, способные привести к исключению и к появлению "мусора"> 
finally
<операторы, выполняемые в любом случае и производящие зачистку "мусора"> 
end;



В этом случае операторы в разделе finally будут выполняться всегда, независимо от того, было или не было исключение. После выполнения этих операторов вычисления, как и ранее, прерываются. Если в блоке try были открыты какие-то временные файлы, динамически выделялась память под временные объекты, выполнялись соединения с какими-то базами данных, а после всего этого произошла ошибка, вызвавшая исключение, то в блоке finally можно убрать весь "мусор": удалить ненужные временные файлы, освободить память от временных объектов, разорвать связь с базой данных.
К сожалению, остаются другие из рассмотренных проблем: необходимость принять какие-то меры для дальнейшей нормальной работы программы при генерации исключения, а также необходимость уведомить пользователя о желательных действиях с его стороны. Решить эти проблемы в данном случае невозможно, поскольку при выполнении операторов раздела finally программа не знает, произошло ли исключение и если произошло, то какое именно.
Рассмотренный способ использования блока try ... finally позволяет защитить ресурсы программы внутри блока. Существует также способ глобального выделения и инициализации ресурсов, с защищенной их очисткой. Он состоит в использовании двух специальных включаемых в модуль разделов initialization и finalization. Раздел initialization, выполняемый один раз при первом упоминании в программе данного модуля, можно использовать для однократного выделения необходимых глобальных ресурсов. А раздел finalization, гарантированно выполняемый в конце работы программы независимо от наличия или отсутствия исключений, можно использовать для очистки этих и других ресурсов, затребованных при работе программы. Например, если в процессе работы программы временные файлы с расширениями .tmp могут создаваться в каталоге, имя которого записано в переменной sdirtmp, то гарантированно удалить их при завершении работы программы можно следующими операторами:
var 
sSR:TSearchRec; F:File;
ires:integer;
….
initialization
….
finalization 
ires := FindFirst(sdirtmp+'*.tmp', faAnyFile, sSR); 
while ires=0 do 
begin 
AssignFile(F, sdirtmp+sSR.name); 
Erase(F);
ires := FindNext(sSR) 
end;

end.




В этом фрагменте программы с помощью процедуры Erase из каталога, имя которого записано в переменной sdirtmp, удаляются все файлы с расширением .tmp. Поиск этих файлов осуществляется функциями FindFirst и FindNext.
В разделе finalization можно определить, завершается ли программа нормально или в результате генерации исключения, проверив функцию ExceptAddr.
Следует учесть, что включение в модуль раздела finalization возможно только при наличии в нем раздела initialization. Даже если он не требуется, нужно включить в программу хотя бы пустой раздел initialization, если предполагается использовать очистку ресурсов в разделе finalization.
Наиболее кардинальным способом борьбы с исключениями является третий из вышеупомянутых - обработка исключений с помощью блоков try ... except. Синтаксис этих блоков следующий:
try
<Исполняемый код> 
except
<Код, исполняемый в случае ошибки>; 
end;



Операторы раздела except выполняются только в случае генерации исключения в операторах блока try. В результате при возникновении исключения появляется возможность предпринять какие-либо действия: известить пользователя о возникшей проблеме и подсказать ему пути ее решения, принять какие-то меры к исправлению ошибки (например, при делении на нуль заслать в результат очень большое число соответствующего знака) и т.д.
Важным является то, что в разделе except можно определить тип сгенерированного исключения и дифференцированно реагировать на различные исключительные ситуации. Это делается с помощью оператора:
on <класс исключения> do <оператор>;



В языке Object Pascal исключительные ситуации представляют собой объекты, содержащие информацию, идентифицирующую ошибку и место ее возникновения. Внутри раздела except создаются обработчики особых ситуаций для классов исключительных ситуаций. Обработчик особой ситуации имеет следующий формат:
try
<Исполняемый код>
except
on E: ESomeException do <обработчик исключения>; 
end;



Для обработки особой ситуации Delphi предоставляет возможность создания временных объектов особой ситуации (Е), которые могут использоваться в обработчике исключения. Временный объект особой ситуации имеет тот же тип, что и объект исключения, который он обозначает (ESomeException).
Существуют предопределенные классы исключительных ситуаций для обработки стандартных ошибок, таких как нехватка памяти, деление на нуль, числовое переполнение, ошибки ввода-вывода и другие. Некоторые из них следующие:
1. EMathError - класс-предок исключений, случающихся при выполнении операций с плавающей точкой; 
EInvalidArgument - значение параметра выходит за диапазон значений 
EInvalidOp - передача математическому сопроцессору ошибочной конструкции; 
EOverflow - переполнение разрядов при работе со слишком большими величинами;
EUnderflow - потеря разрядов при работе со слишком малыми величинами;
EZeroDivide - деление на ноль.
2. EIntError - класс-предок исключений, случающихся при выполнении целочисленных операций; 
EDivByZero - деление на ноль;
EIntOverflow - выполнение операций, приводящих к переполнению целых переменных;
ERangeError - значение целочисленного выражения выходит за пределы установленного целочисленного типа. Попытка обращения к элементу массива по индексу, выходящему за пределы массива. 
3. EListError - обращение к элементу списка (String, List, TStringList) по индексу, выходящему за пределы допустимых значений.
4. EInOutError - ошибки при операциях с файловой системой. Код ошибки возвращается в локальной переменной ErrorCode, которая может принимать следующие значения:
2 - файл не найден;
3 - неверное имя файла;
4 - слишком много открыто файлов;
5 - отказ в доступе;
100 - конец файла;
101 - диск полон;
106 - неверная операция ввода. 



5. EConvertError - ошибки преобразования типов (простых, объектных).
Операторы on...do позволяют проводить выборочную обработку различных исключений. Пример такой обработки:
var A : shortint;

try

С := StrToInt(Editl.text); A := В div С;
except 
on EConvertError do 
MessageDlg('Вы ввели ошибочное число; повторите ввод', mtWarning, [mbOk], 0); 
on EDivByZero do 
MessageDlg('Вы ввели нуль; повторите ввод', mtWarning, [mbOk], 0) ; 
on EIntOverflow do
if (B*C) >= 0 then A := 32767 else A := -32767;

end;



В этом примере выполняются чтение целого числа, введенного пользователем в окно редактирования Editl, и деление на него переменной В. Если пользователь ввел не целое число (например, по ошибке нажал не цифру, а какой-то буквенный символ), то при выполнении функции StrToInt возникнет исключение класса EConvertError. Соответствующий обработчик исключения сообщает пользователю о сделанной ошибке и советует повторить ввод. Аналогичная реакция следует на ввод пользователем нуля (класс исключения EDivByZero). Если же при делении возникает целочисленное переполнение (класс исключения Elnt-Overflow), то результату присваивается максимальное положительное или отрицательное значение.
Некоторые исключения имеют дополнительные поля (свойства), уточняющие вид ошибки. Например, это относится к исключению файлового ввода/вывода EInOutError, которое имеет свойство errorcode типа integer. Это свойство содержит стандартный для Object Pascal номер ошибки файлового ввода/вывода.
Чтобы воспользоваться полями исключений, оператор on надо записывать в виде
on <имя>: <класс исключения> do
<операторы с конструкциями <имя>.<имя поля>>;



Содержащееся в этом операторе <имя> носит сугубо локальный характер, нигде ранее определяться не должно и вводится только для того, чтобы можно было сослаться на поле по имени объекта исключения. 
Пример использования полей при операциях файлового ввода/вывода (подразумевается, что в переменной filename типа string хранится имя обрабатываемого файла):
on IO: EInOutError do 
begin 
Case IO.errorcode of
2: s:='Файл ''' +s+''' не найден';
3: s:='Ошибочное имя файла ''' + s+ ' ' ' ' ;
4: s:='Слишком много открытых файлов';
5: s:='Файл ''' +s+''' не доступен';
100: s:= 'Достигнут конец файла ''' +s+'"'";
101: s:= 'Диск переполнен при работе с файлом ''' + s + ' ' ' ' ; 
106: s:= 'Ошибка ввода при работе с файлом ' ' ' +s+ ' ' ' ' ; 
end;
MessageDlg(s, mtWarning, [mbOk], 0); 
end;



Приведенный код анализирует поле исключения и выдает сообщение о виде ошибки.
Помимо операторов on, обрабатывающих заданные типы исключений, в раздел except может быть включен оператор else, в котором выполняется обработка всех не перехваченных ранее исключений, т.е. происходит обработка по умолчанию. Например:
except 
on . . . ; 
on . . . ;
else <обработчик всех не перехваченных ранее событий>; 
end;




Оператор else должен идти последним, так как в противном случае все обработчики, записанные после него, работать не будут, поскольку все исключения уже будут перехвачены. Внутри обработчика по умолчанию можно получить доступ к объекту исключения, воспользовавшись функцией ExceptObject.
В раздел except могут включаться или только операторы on, или только какие-то другие операторы. Смешение операторов on с другими не допускается.
Раздел except может включаться совместно с описанным ранее разделом finally. Например, можно организовать блоки try следующим образом:
try

try

finally

end; 
except

end;



Блоки try...except могут быть вложенными явным или неявным образом. Примером неявной вложенности является блок try...except, в котором среди операторов раздела try имеются вызовы функций или процедур, которые имеют свои собственные блоки try...except. Рассмотрим последовательность обработки исключений в этих случаях. При генерации исключения сначала ищется соответствующий ему обработчик on в том блоке try...except, в котором создалась исключительная ситуация. Если соответствующий обработчик не найден, поиск ведется в обрамляющем блоке try...except (при наличии явным образом вложенных блоков) и т.д. Если в данной функции или процедуре обработчик не найден или вообще в ней отсутствуют блоки try...except, то поиск переходит на следующий уровень - в блок, из которого была вызвана данная функция или процедура. Этот поиск продолжается по всем уровням. И только если он закончился безрезультатно, выполняется стандартная обработка исключения, заключающаяся, как уже было сказано, в выдаче пользователю сообщения о типе исключения.
Как только оператор on, соответствующий данному исключению, найден и выполнен, объект исключения разрушается и управление передается оператору, следующему за соответствующим блоком try...except.
Возможен также вариант, когда в самом обработчике исключения в процессе обработки возникла исключительная ситуация. В этом случае обработка прерывается, прежнее исключение разрушается и генерируется новое исключение. Его обработчик ищется в блоке try...except, внешнем по отношению к тому, в котором возникло новое исключение.
Например, для создания устойчивого к ошибкам приложения, записывающего данные в файл, можно использовать следующий код:
begin
assignfile(f, 'data.dan'); 
try 
append(f); 
try 
writeln (f,s); 
finally
closefile(f) ; 
end; 
except 
on E: EInOutError do 
if E.ErrorCode = 2 then 
if MessageDlg('Файл не найден. Создать его?', mtError, [mbYes, mbNo],0)=mrYes
then FileCreate('data.dan'); 
end; 
end;



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

Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=30127