Глава 4

Обработка исключительных ситуаций

Любому программисту, работавшему над более или менее сложным проектом, интуитивно ясно, что такое обработка исключительных ситуаций. Любое взаимодействие с операционной системой на предмет получения ресурсов — места на диске, в оперативной памяти, открытие файла — может завершиться неудачно. Любое вычисление может закончиться делением на ноль или переполнением.

Платой за надежную работу программы в таких условиях служит введение многочисленных проверок, способных предотвратить некорректные действия в случае возникновения нештатной ситуации. Хорошо, если в конце очередной конструкции if. . .then можно просто поставить оператор Exit. Обычно же для корректного выхода из ситуации нужно отменить целую последовательность действий, предшествующих неудачному. Все это сильно запутывает программу, маскируя четкую структуру главного алгоритма.

Выход из положения предложен разработчиками Windows NT и впоследствии внесен во все операционные системы от Microsoft. Имя ему — структурированная обработка исключительных ситуаций (ИС). Постепенно ее поддержка появилась и в ведущих системах программирования, в том числе — в Delphi.

Операторы try..except и try..fmally

Что же такое исключительная ситуация? Интуитивно понятно, что это — некое нештатное событие, могущее повлиять на дальнейшее выполнение программы. Так вот, современный компилятор дописывает код, который перехватывает любое такое событие, предотвращает дальнейшие нежелательные последствия, сохраняет необходимые данные о состоянии программы, и выдает разработчику... Что можно выдать в объектно-ориентированном языке программирования? Конечно же, объект. С точки зрения Object Pascal исключительная ситуация — это объект. Для работы с таким особенным объектом существуют специальные конструкции языка —блоки try...except И try..finally.

Для реакции на конкретный тип ситуации применяется блок try..except. Синтаксис его следующий:

try <0ператор> <0ператор>

except

on Exceptioni do < Оператор >;

on Exception2 do < Оператор >;

else { }

<0ператор> {обработчик прочих ИС} end;

Выполнение блока начинается с секции try. При отсутствии исключительных ситуаций только она и выполняется. Секция except получает управление в случае возникновения ИС. Обработчик ИС состоит из набора директив on... do, определяющих реакцию приложения на определенную ситуацию. Каждая директива связывает ситуацию (on...), заданную своим именем, с группой операторов (do...).

try

U := 220.0;

R := 0;

I := U / R;

except

on EZeroDivide do MessageBox('Короткое замыкание!');

end;

В этом примере замена if.. .then на try.. .except внешне не дала очевидной экономии кода. Однако если при решении, допустим, вычислительной задачи проверять на возможное деление на ноль приходится не один, а множество раз, то выигрыш от нового подхода неоспорим — достаточно одного блока на все вычисления.

При возникновении ИС директивы просматриваются последовательно, в порядке их описания. Каждый тип исключительной ситуации, описанный после ключевого слова on, обрабатывается именно этим блоком: только то, что предусмотрено в нем, и будет являться реакцией на данную ситуацию.

Если возникла ситуация, не определенная ни в одной из директив, выполняются те операторы, которые стоят после else. Если и их нет, то ИС считается не обработанной и будет передана на следующий уровень обработки. Этим следующим уровнем может быть другой оператор try. .except, который содержит в себе данный блок. На самом верхнем уровне вызывается обработчик ИС по умолчанию, предусмотренный в стандартном модуле sysutils.pas. После обработки происходит выход из защищенного блока, и управление обратно в секцию try не передается. Разумеется, если ИС произошла вне блока try. .except, это не обязательно приведет к аварийному завершению всего приложения. Обработчики по умолчанию сделаны так, что в этом случае заканчивается лишь выполнение метода, внутри которого возникла ИС. Стандартная обработка подразумевает вывод на экран панели текстового сообщения (из свойства Exception. Message) с указанием типа ошибки. Можно получить и развернутую информацию с именем модуля и адреса, где она имела место (рис. 4.1).

Рис. 4.1. Типовое окно сообщения об ошибке

Для этого нужно вызвать функцию ExceptionErrorMessage, имеющуюся в модуле sysutils. pas. Подробнее о ней рассказано в разделе "Работа со свойствами исключительных ситуаций" этой главы.

Если предусмотренной вами обработки ИС недостаточно, то можно продолжить ее дальше программно, при помощи оператора raise:

s1:= TStringList.Create;

try

s1.LoadFromFile(AFileName);

except

s1.Free;

raise;

end;

Здесь в случае возникновения исключительной ситуации созданный список строк должен быть уничтожен. Сама же обработка предоставляется "вышестоящим инстанциям".

Параллельно с блоком try. .except в языке существует и try. .finally. Он соответствует случаю, когда необходимо возвратить выделенные программе ресурсы даже в случае аварийной ситуации. Синтаксис блока try.. .finally таков:

try

<0ператор> <0ператор>

finally <0ператор>

end;

Следующие за try операторы выполняются в обычном порядке. Если за это время не возникло никаких ИС, далее выполняются те операторы, которые стоят после finally. В случае, если между try и finally произошла ИС, управление немедленно передается на оператор(ы) после finally, которые называются кодом очистки. Допустим, вы поместили после try операторы, которые должны выделить вам ресурсы системы (дескрипторы блоков памяти, файлов, контекстов устройств и т. п.). Тогда операторы, освобождающие их, следует поместить после finally, и ресурсы будут освобождены в любом случае. Блок try. .finally, как можно догадаться, еще называется блоком защиты ресурсов.

Важно обратить внимание на такой факт: данная конструкция ничего не делает с самим объектом— исключительной ситуацией. Задача try.. finally -только прореагировать на факт нештатного поведения программы и проделать определенные действия. Сама же ИС продолжает "путешествие" и вопрос ее обработки остается на повестке дня.

Блоки защиты ресурсов и обработчики ИС, как и другие блоки, могут быть вложенными. В этом простейшем примере каждый вид ресурсов системы защищается в отдельном блоке:

try AllocatelstResource;

try Allocate2ndResource;

SolveProblem;

finally FreeZndResource;

end;

finally FreelstResource;

end;

Можно также вкладывать обработчики друг в друга, предусмотрев в каждом специфическую реакцию на ту или иную ошибку:

var i,j,k : Integer;

begin

i := Round(Random);

j := 1 -i;

try

k := 1 div i;

try

k := 1 div j;

except

On EDivByZero do ShowMessage('Вариант 1') ;

end;

except

On EDivByZero do

ShowMessage('Вариант 2');

end;

end;

Но все же самый распространенный случай — это сочетание блоков двух

типов. В один из них помещается общее (освобождение ресурсов в finally), в другой — особенное (конкретная реакция внутри except).

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

Чем же различаются между собой исключительные ситуации? Как отличить Exceptioni от Exception2? Поскольку это объекты, они отличаются классом (объектным типом). Объектный тип Exception описан в модуле sysutils. pas. Он является предком для всех других объектов— исключительных ситуаций.

Exception = class(TObject) public

constructor Create(const Msg: string);

constructor CreatePtnt(const Msg: string; const Args: array of const);

constructor CreateRes(Ident: Word);

constructor CreateResFmt(Ident: Word; const Args: array of const);

constructor CreateHelp(const Msg: string; AHelpContext: Longint);

constructor CreateFtntHelp (const Msg: string; const Args: array of const;

AHelpContext! Longint);

constructor CreateResHelp(Ident: Word; AHelpContext: Longint);

constructor CreateResFmtHelp(Ident: Word; const Args: array of const;

AHelpContext: Longint) ;

property HelpContext: Longint;

property Message: string;

end;

ExceptClass = class of Exception;

Как видно из приведенного описания класса Exception, у него имеется восемь конструкторов, позволяющих задействовать при создании объекта текстовые строки из ресурсов приложения, форматирование текста, связь с контекстом справочной системы.

Конструкторы, в названии которых встречается подстрока fmt, могут вставлять в формируемый текст сообщения об ошибке значения параметров, как это делает стандартная функция Format:

If MemSize>Limit then

raise EOutOfMemory.CreateRnt('Cannot allocate more than %d bytes',[Limit]);

Если в названии присутствует подстрока Res, это означает, что текст сообщения будет загружаться из ресурсов приложения. Это особенно полезно при создании локализованных версий программных продуктов, когда нужно сменить язык всех сообщений, ничего не перекомпилируя.

И наконец, если в названии фигурирует подстрока Help, то такой конструктор инициализирует свойство HelpContext создаваемого объекта. Естественно, система помощи должна быть создана и в ней должна иметься статья, связанная с этим контекстом. Теперь пользователь может затребовать помощь для данной ситуации, скажем, нажав клавишу <F1> в момент показа сообщения об ИС.

Тип Exception порождает многочисленные дочерние типы, соответствующие часто встречающимся случаям ошибок ввода/вывода, распределения памяти и т. п. Заметим, что Exception и его потомки представляют собой исключение из правила, предписывающего все объектные типы именовать с буквы т. Потомки Exception начинаются С Е, Например, EZeroDivide.

Ниже приведены таблицы, содержащие несколько важных групп исключительных ситуации.

Таблица 4.1. ИС работы с памятью (порождены отЕНеарЕхсерNоп)

Тип

Условие возникновения

EOutOfMemory

Недостаточно места в памяти

EOutOfRe sources

Нехватка системных ресурсов

EinvalidPointer

Недопустимый указатель (обычно nil)

 

Таблица 4.2. ИС целочисленной математики (порождены от EintError)

Тип

Условие возникновения

EdivByZero

Попытка деления на ноль (целое число)

ErangeError

Число или выражение выходит за допустимый диапазон

EintOverflow

Целочисленное переполнение

 

Таблица 4.3. ИС математики с плавающей точкой (порождены от EMa thError)

Тип

Условие возникновения

EInvalidOp

Неверная операция

EZeroDivide

Попытка деления на ноль

EOverflow

Переполнение с плавающей точкой

EUnderflow

Исчезновение порядка

EInvalidArgument

Неверный аргумент математических функций

Все же самое важное в объекте Exception — это класс, к которому он принадлежит. Именно факт принадлежности возникшей ИС к тому или иному классу говорит о том, что случилось. Если же нужно детализировать проблему, можно присвоить значение свойству Message. Если и этого мало, можно добавить в объект новые поля. Так, в ИС EinOutError (ошибка ввода/вывода) есть поле ErrorCode, значение которого соответствует произошедшей ошибке — запрету записи, отсутствию или повреждению файла, и т. д.

try

except on E: EinOutError do case E.ErrorCode of

2: ShowMessage('Файл не найден!');

5: ShowMessage('Доступ запрещен!');

101: ShowMessage('Диск переполнен! ');

end;

end;

Впрочем, ИС EinOutError возникают только тогда, когда установлен параметр компилятора {$iochecks on} (или иначе ($I+}). В противном случае проверку переменной lOResult (известной еще по Turbo Pascal) нужно делать самому.

Не следует забывать, что в директиве on указываются имена классов. Если обработчик родительского класса стоит перед дочерним, последний никогда не получит управления.

try i:=l;j:=0;

k:=i

div j;

except on EIntError do ShowMessage('IntError');

on EDivByZero do ShowMessage('DivByZero');

end;

В этом случае, хотя в действительности будет иметь место деление на ноль (EDivByZero), вы увидите сообщение, соответствующее родительскому классу EIntError. Но стоит поменять две конструкции on.. do местами, и все придет в норму.

Многие важнейшие классы VCL — списки, потоки, графические объекты — сигнализируют о своих (ваших?) проблемах созданием соответствующей

ИС — EListError, EInvalidGraphic, EPrinter И Т. Д.

Алгоритм обработки исключительных ситуаций

Если произошла ошибка и активизирована исключительная ситуация, то она будет обрабатываться по такому алгоритму:

  1. Если ситуация возникла внутри блока try. .except или try. .finally, то там она и будет обработана. В случае finally, а также если ИС "продвинута" дальше при помощи оператора raise, обработка продолжается.
  2. Если программистом определен обработчик события Application.onException, то он получит управление. Обработчик объявлен следующим образом:
  3. TExceptionEvent = procedure (Sender: TObject; E: Exception) of object;

  4. Если программист никак не определил реакцию на ИС, то будет вызван стандартный метод showException, который сообщит о классе и месте возникновения исключительной ситуации.

Пункты 2 и 3 реализуются в методе TAppiication.HandleException. Собственно, выглядят они следующим образом:

if not (ExceptObject is EAbort) then

if Assigned(FOnException) then

FOnException(Sender, Exception(ExceptObject)) else ShowException(Exception(ExceptObject));

Обработчик onException нужен, если требуется выполнять одно и то же действие в любой исключительной ситуации, возникшей в вашем приложении. К примеру, назвать себя, указать координаты для обращения или предупредить, что это еще бета-версия.

В Delphi 5, в отличие от предыдущих версий, получить доступ к обработчику OnException очень просто. Достаточно добавить в форму невизуальный компонент TApplicationEvent и назначить обработчик соответствующему событию.

procedure TForml.ApplicationEventslException(Sender: TObject;

E: Exceptions-Begin ShowMessage('Произошла исключительная ситуация ' + E.ClassName

+ ': ' + E.Message

+ #13#10'Свяжитесь с разработчиками по тел. 222-33-44');

end;

Теперь пользователь знает, что благодарить за неожиданности нужно по указанному в сообщении об ошибке телефону разработчиков.

Можно пойти дальше и закрыть форму, в которой возникла необработанная исключительная ситуация. Для этого в обработчике onException нужно, во-первых, отыскать ее, и, во-вторых, принудительно закрыть, отключив обработку cобытий OnClose и OnCloseQuery.

 

Листинг 4.1 Принудительное закрытие формы, вызвавшей ИС

var ParentChain : strings;

function TForml.FindSenderFormfSender: TObject): TForm;

var

С, СР: TControl;

 

begin

ParentChain := '' ;

Result := nil;

if not Assigned(Sender) then

Exit;

if not (Sender is TControl) then

Exit ;

С:= Sender as TControl;

CP := C.Parent;

while not (C is TForm) and Assigned(CP) do begin

C:= CP;

if ParentChain = '' then ParentChain := C.Name

else

ParentChain := ParentChain + '->' + C.Name;

CP := CP.Parent;

end;

if C is TForm then

Result := C as TForm else

Result := nil end;

procedure TForml.CloseFormCausingException(Sender: TObject);

var SenderForm : TForm;

begin

SenderForm := FindSenderForm(Sender) ;

if As signed(SenderForm) then begin

// to disable the form's protection against closing

if Addr((SenderForm as TForm).OnCloseQuery) <> nil then

(SenderForm as TForm).OnCloseQuery := nil;. if Addr((SenderForm as TForm).OnClose) <> nil then (SenderForm as TForm).OnClose := nil;

// We use the PostMessage instead of SendMessage PostMessage(SenderForm.Handle, WM_CLOSE, 0, 0);

end end;

procedure TFormI.ApplicationEventslException(Sender: TObject;

E: Exception) ;

begin

CloseFormCausingException(Sender) ;

ShowMessage(ParentChain+' '+E.Message) ;

end;

Работа со свойствами исключительных ситуаций

Как можно "пощупать" переданный при исключительной ситуации объект? Если во время обработки требуется доступ к свойствам этого объекта, его нужно назвать внутри on.. do, указав перед именем класса некий идентификатор:

on EZD: EZeroDivide do EZD.Message := 'Деление на ноль!';

Здесь возникшее исключение выступает под именем ezd. Можно изменить его свойства и отправить дальше:

var APtr : Pointer;

Formi : TForm;

try

APtr := Formi;

with TObject(APtr) as TBitmap do;

except on EZD: EInvalidCast do EZD.Message := . EZD.Message + 'xa-xa!';

Raise;{ теперь обработка будет' сделана в другом месте } end;

Но как назвать исключительную ситуацию, не попавшую ни в одну из директив on. .do? Или, может быть, в вашем обработчике вообще нет on. .do, a поработать с объектом надо? Описанный выше путь здесь не подходит. Для этих случаев есть пара системных функций Exceptobject и ExceptAddr. К сожалению, эти функции инициализируются только внутри конструкции try. .except; в try. .finally работать с объектом — исключительной ситуацией не представляется возможным.

Часто нужно иметь подробный материал для анализа причин возникновения ИС. Разумно было бы записывать все данные о них в файл, чтобы потом прогнозировать ситуацию. Такой подход важен для программ, которые, так или иначе, будут отчуждены от разработчика: в случае возникновения непредвиденной ситуации это позволит ответить на вопросы "кто виноват?" и "что делать?". В следующем примере предложен вариант реализации протоколирования ИС.

const LogName : string = 'c:\appexc.log';

procedure LogException;

var fs: TFileStream;

m : word;buf : array[0..511] of char;

begin

if FileExists(LogName) then m := fmOpenReadWrite

else m:= fmCreate;

fs:= TFileStream.Create(LogName,m);

fs.Seek(0,soFromEnd);

StrPCopy(Buf,DateTimeToStr (Now) +'. ');

ExceptionErrorMessage (ExceptObject,ExceptAddr,

@buf[StrLen(buf)],SizeOf(Buf)-StrLen(buf)) ;

StrCat(Buf,ttl3ftlO);

fs.WriteBuffer(Buf, StrLen(buf));

fs.Free;

end;

procedure TFoirtil .ButtonlClick( Sender: TObject);

var x,y,z: real;

begin try

try

x: =1. 0; у: =0 .0;

z := х/у;

except LogException;

raise;

end;

except

on E:EIntError do ShowMessage('IntError');

on E:EMathError do ShowMessage('MathError');

end;

end;

Здесь задачу записи информации об ИС решает процедура LogException. Она открывает файловый поток и пишет туда информацию, отформатированную при помощи уже упоминавшейся функции ExceptionErrorMessage.

В качестве ее параметров выступают значения функций Excepiobject и ExceptAddr. К сформированной строке добавляется время возникновения ИС. Для каждого защищаемого блока кода создаются две вложенные конструкции try. .except. Первая, внутренняя — для вас; в ней ИС протоколируется и продвигается дальше. Внешняя — для пользователя; именно в ней проводится анализ типа ИС и готовится сообщение.

Может возникнуть ситуация, когда ИС возникает в цикле, или, хуже того — вложенная ИС. Чтобы в этом случае избежать зацикливания, переполнения файлов протоколов, повторения одних и тех же действий и т. д., нужно сделать так: после возникновения первой ИС обработчик onException подменить на другой, содержащий только минимум операций, например:

procedure TForml.AppTerminatingException(Sender: TObject; E: Exception);

begin

LogException;

Application.OnException := AppFinalException;

E.Message := 'Application Terminated:'#13+E.Message;

Application.ShowException(E);

end;

procedure TForml.AppFinalException(Sender: TObject; E: Exception);

begin Halt;

end;

B Object Pascal существует расширенный вариант употребления оператора raise:

raise <конструктор объектного типа> [at <адрес>]

Естественно, объектный тип должен быть порожден от Exception. To, что в таком типе ничего не переопределено, не столь важно — главное, что в обработчике ИС можно отследить именно этот тип.

ELoginError = class(Exception);

If LoginAttemptsNo > MaxAttempts then raise ELoginError.Create('Ошибка регистрации пользователя') ;

Конструкция at <адрес> используется для того, чтобы изменить адрес, к которому привязывается возникшая ИС, в пределах одного блока обработки ИС.

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

Если вы внимательно просмотрели код HandleException, то увидели там упоминание класса EAbort. Исключительная ситуация EAbort служит единственным — и очень важным — исключением из правил обработки. Она называется "тихой" (Silent) и отличается тем, что для нее обработка по умолчанию не предусматривает вывода сообщений на экран. Естественно, все сказанное касается и порожденных от нее дочерних объектных классов.

Применение EAbort оправдано во многих случаях. Вот один из примеров. Пусть разрабатывается некоторая прикладная программа, или некоторое семейство объектов, не связанное с VCL. Если в них возникает ИС, то нужно как-то известить об этом пользователя. А между тем прямой вызов для этого функции showMessage или меззадевох не всегда оправдан. Для маленькой и компактной динамической библиотеки не нужно тащить за собой громаду VCL ради выдачи сообщения. С другой стороны, в большом и разнородном проекте нельзя давать каждому объекту или подпрограмме право самой общаться с пользователем. Если их разрабатывают разные люди, такой проект может превратиться в вавилонскую башню. Тут и поможет EAbort. Эта исключительная ситуация не возникает в недрах системы — ее должен создавать и обслуживать программист.

EAbort — реальная альтернатива многочисленным конструкциям if. .then и тем более (упаси Боже!) goto. Она нужна, если вы сами видите, что сложились определенные условия и пора менять логику работы программы.

If Logical-Condition then Raise EAbort. Create ('Condition 1');

Если не нужно задавать сообщение, можно создать EAbort, и проще — вызвать процедуру Abort (без параметров), содержащуюся в модуле sysutils . раз.

Настройка отладчика и обработчика ИС в среде Delphi

В пятой версии Delphi расширены возможности протоколирования событий, происходящих с вашим приложением. Журнал событий (Event Log) получает события как во время компиляции, так и во время выполнения (рис. 4.2).

Установка флажка Breakpoint messages ведет к тому, что в журнал записываются сообщения о достижении точек прерывания обо всех возникших ИС. В группу Process messages включены сообщения о загрузке и выгрузке приложения и его модулей.

Флажок Output messages позволяет включать в журнал отладочные сообщения программы. Чтобы сгенерировать сообщение, нужно вызвать функцию Windows API:

procedure OutputDebugString(IpOutputString: PChar);

Наконец, флажок Window messages может служить заменой утилиты WinSight, до сих пор поставляемой с Delphi. Если он установлен, то в Журнал записываются все сообщения Windows, получаемые всеми окнами вашей программы.

Если вы не отдаете себе отчет в том, сколько сообщений получает каждое окно во время работы программы (а их тысячи), то лучше не пытайтесь установить этот флажок — информация окажется бесполезной. Остается пожелать разработчикам из Inprise, чтобы они включили в следующую версию Delphi фильтр для выбора определенных типов сообщений.

Рис. 4.2. Страница параметров Журнала событий (Event Log)

Если вы хотите проанализировать полученные сообщения в спокойной обстановке, во всплывающем меню окна Журнала событий есть возможность записать его содержимое в выбранный вами файл.

В Delphi 5, помимо EAbort, среда может игнорировать и другие типы исключительных ситуаций. Вызвав диалоговое окно Tools/Debugger options, вы можете составить нужный вам список ИС. По умолчанию в нем присутствуют EAbort и ИС, генерируемые Microsoft DAO, VisiBroker и CORBA.

Среда Delphi по умолчанию перехватывает все возникающие ИС. Зачастую это помогает. Но в ряде случаев, особенно если у вас предусмотрена своя обработка, это может расстроить все планы. В меню Tools/Debugger Options... нужно отключить флажок Stop on Delphi Exceptions, и среда Delphi перестает реагировать на все ИС, возникающие в отлаживаемой программе.

Процедура Assert

Эта процедура и сопутствующая ей ИС EAssertionFailed специально перенесены в Object Pascal из языка С для удобства отладки. Синтаксис ее прост:

procedure Assert(expr : Boolean [; const msg: string]);

При вызове функции проверяется, чему равно значение переданного в нее булевого выражения ехрr. Если оно равно True, то ровным счетом ничего не происходит. Если же оно равно False, создается ИС EAssertionFailed. Все это было бы довольно тривиально с точки зрения уже изученного, если бы не два обстоятельства:

Рис. 4.3. Окно сообщения обработчика исключительной ситуации EAssertionFailed

Резюме

Применение обработки исключительных ситуаций в повседневной практике программирования способно дать революционные результаты. Это один из лучших приемов, позволяющих существенно уменьшить число ошибок в программах. Вы сможете быстро определить, где, что и почему произошло; а раз так — и быстро устранить причины. Умение работать с исключительными ситуациями пригодится вам в следующих главах: