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


Любому, кто писал более или менее сложные программы, интуитивно ясно, что такое обработка исключительных ситуаций (ИС). Всякое взаимодействие с операционной системой на предмет получения ресурсов — места на диске, в памяти, открытие файла — может завершиться неудачно. Любое вычисление может закончиться делением на ноль или переполнением. Дополнительный фактор возникновения исключительных ситуаций содержится в данных, к которым могут обращаться программы. Особенно это актуально в приложениях баз данных.
Платой за надежную работу программы в таких условиях служит введение многочисленных проверок, способных предотвратить некорректные действия в случае возникновения нештатной ситуации. Хорошо, если в конце очередной конструкции if..then можно просто поставить оператор Exit. Обычно же для корректного выхода из ситуации нужно отменить целую последовательность действий, предшествующих неудачному. Все это сильно запутывает программу, маскируя четкую структуру главного алгоритма.
При разработке приложений в Delphi программист имеет возможность использовать несколько механизмов, обеспечивающих обработку исключительных ситуаций. Это и специальные операторы языка Object Pascal, и классы, предназначенные для программирования реакции на ошибки.
Поэтому эта глава посвящена... нет, не тому, как писать безошибочно; а тому, как защищать приложения от воздействия неизбежно возникающих ошибок.
Исключительная ситуация как класс
Что же такое исключительная ситуация? Интуитивно понятно, что это — некое нештатное событие, могущее повлиять на дальнейшее выполнение программы. Если вы ранее писали в среде Turbo Pascal или подобной, то вы
наверняка пытались избежать таких ситуаций, вводя многочисленные проверки данных и кодов возврата функций. От этого громоздкого кода можно раз и навсегда избавиться, взяв на вооружение механизм, реализованный в Delphi.
Компилятор Delphi генерирует код, который перехватывает любое такое нештатное событие, сохраняет необходимые данные о состоянии программы, и выдает разработчику... Что можно выдать в объектно-ориентированном языке программирования? Конечно же, объект. С точки зрения Object Pascal исключительная ситуация — это объект.
Вы можете получить и обработать этот объект, предусмотрев в программе специальную языковую конструкцию (try. .except). Если такая конструкция не предусмотрена, все равно исключение будет обработано — в недрах VCL есть соответствующие обработчики, окружающие все потенциально опасные места.
Чем же различаются между собой исключительные ситуации? Как отличить одну исключительную ситуацию от другой? Поскольку это объекты, они отличаются классом (объектным типом). В модуле SYSUTILS.PAS описан объектный тип Exception. Он является предком для всех других объектов — исключительных ситуаций. Вот он:
Exception = class(TObject)
 private
FMessage: string;
  FHelpContext: Integer;
 public
constructor Create(const Msg: string);
constructor CreateEmt(const Msg: string; const Args: array of const);
constructor CreateRes(Ident: Integer); overload;
constructor CreateRes(ResStringRec: PResStringRec); overload;
constructor CreateResFmt(Ident: Integer; const Args: array of const);
overload; constructor CreateResFmt(ResStringRec: PResStringRec; const Args: array of const); 
overload;
constructor CreateHelp(const Msg: string; AHelpContext: Integer);
 constructor CreateFmtHelp(const Msg: string; const Args:
array of const;
AHelpContext: Integer);
 constructor CreateResHelp(Ident: Integer; AHelpContext: Integer);
overload;
constructor CreateResHelp(ResStringRec: PResStringRec; AHelpContext: Integer); overload;
constructor CreateResFmtHelp(ResStringRec: PResStringRec; const Args: array of const; AHelpContext: Integer); overload;
constructor CreateResFmtHelp(Ident: Integer; const Args: array of const; AHelpContext: Integer); overload;
 property HelpContext: Integer read FHelpContext write FHelpContext;
 property Message: string read FMessage write FMessage;
  end;
ExceptClass = class of Exception;
Как видно из приведенного описания класса Exception, у него имеется двенадцать (!) конструкторов, позволяющих задействовать при создании объекта текстовые строки из ресурсов приложения (имя включает строку Res), форматирование текста (включает Fmt), связь с контекстом справочной системы (включает Help).
Конструкторы, в названии которых встречается подстрока Fmt, могут вставлять в формируемый текст сообщения об ошибке значения параметров, как это делает стандартная функция Format:
If MemSize > Limit then
raise EOutOfMemory.CreateFmt('Cannot allocate more than %d 
bytes',[Limit]);
Если в названии присутствует подстрока Res, это означает, что текст сообщения будет загружаться из ресурсов приложения. Это особенно полезно при создании локализованных версий программных продуктов, когда нужно сменить язык всех сообщений, ничего не компилируя заново.
И наконец, если в названии фигурирует подстрока Help, то такой конструктор инициализирует свойство HelpContext создаваемого объекта. Естественно, система помощи должна быть создана и в ней должна иметься статья, связанная с этим контекстом. Теперь пользователь может затребовать помощь для данной ситуации, скажем, нажав клавишу <F1> в момент показа сообщения об ИС.
Тип Exception порождает многочисленные дочерние типы, соответствующие часто встречающимся случаям ошибок ввода/вывода, распределения памяти и т. п. Дерево исключительных ситуаций Delphi 7 приведено на 1.
Заметим, что тип Exception и его потомки представляют собой исключение из правила, предписывающего все объектные типы именовать с буквы Т.
Потомки Exception начинаются с Е, например EZeroDivide.
Для экономии места потомки нескольких важных объектов не показаны. Ниже приведены табл. 3.1—3.3, содержащие описания этих групп исключительных ситуаций.
Вы можете самостоятельно инициировать исключительную ситуацию при выполнении тех или иных действий. Но, хотя синтаксис конструктора объекта Exception похож на конструкторы всех других объектов, создается он по- особенному.
Таблица 3.1. Исключительные ситуации при работе с памятью (порождены от EHeapException)


Тип

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

EOutOfMemory

Недостаточно места в куче (памяти)

EOutOfResources

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

EInvalidPointer

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

Таблица 3.2. Исключительные ситуации целочисленной математики (порождены от EIntError)


Тип

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

EDivByZero

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

ERangeError

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

EIntOverflow

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

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


Тип

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

EInvalidOp

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

EZeroDivide

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

EOverflow

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

EUnderflow

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

EInvalidArgument

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

Для этого используется оператор raise, за которым в качестве параметра должен идти экземпляр объекта типа Exception. Обычно сразу за оператором следует конструктор класса ИС:
raise EMathError.Create(' ') ;
но можно и разделить создание и возбуждение исключительной ситуации:
var E: EMathError; 
begin
E := EMathError.Create С');
raise E; 
end;
Оператор raise передает созданную исключительную ситуацию ближайшему блоку try. .except (см. ниже).
if С = 0 then
raise EDivByZero.Create('Деление на ноль')
else
А := В/С;
Самостоятельная инициализация ИС может пригодиться при программировании реакции приложения на ввод данных, для контроля значений переменных и т. д. В таких случаях желательно создавать собственные классы ИС, специально приспособленные для ваших нужд. Также полезно использовать специально спроектированные исключительные ситуации при создании собственных объектов и компонентов. Так, многие важнейшие классы VCL — списки, потоки, графические объекты — сигнализируют о своих (или ваших?) проблемах созданием соответствующей ИС — EListError, EInvalidGraphic, EPrinter и т. д.
Самый важный отличительный признак объекта Exception — это все же класс, к которому он принадлежит. Именно факт принадлежности возникшей ИС к тому или иному классу говорит о том, что случилось. Если же нужно детализировать проблему, можно присвоить значение свойству Message. Если и этого мало, можно добавить в объект новые поля. Так, в ИС EinOutError (ошибка ввода/вывода) есть поле ErrorCode, значение которого соответствует произошедшей ошибке — запрету записи, отсутствию или повреждению файла и т. д.
try
.FileOpenС с:\myfile.txt', fmOpenWrite); 
except
on E: EinOutError do
 case E.ErrorCode of
ERROR_FILE_NOT_FOUND {=2}: ShowMessage('Файл не найден !');
 ERROR_ACCESS_DENIED {=5}: ShowMessage('Доступ запрещен!'); 
ERROR_DISK_FULL {=112}: ShowMessage ('Диск переполнен!') ;
 end;
  end;
Впрочем, ИС EInOutError возникают только тогда, когда установлена опция компилятора {$IOCHECKS ON} (или иначе {$I+}). В противном случае проверку переменной IOResult (известной еще по Turbo Pascal) нужно делать самому.
Еще более "продвинутый" пример — ИС EDBEngineError. Она имеет свойства ErrorCount и свойство-массив Errors: одна операция с базой данных может породить сразу несколько ошибок.
Защитные конструкции языка Object Pascal
Для работы с объектами исключительных ситуаций существуют специальные конструкции языка Object Pascal— блоки try., except и try. .finally. Они контролируют выполнение операторов, помещенных внутри блока до ключевого слова except или finally. В случае возникновения исключительной ситуации штатное выполнение вашей программы немедленно прекращается, и управление передается операторам, идущим за указанными ключевыми словами. Если в вашей процедуре эти блоки отсутствуют, управление все равно будет передано ближайшему блоку, внутри которого возникла ситуация. А уж внутри VCL их предостаточно.
Хотя синтаксис двух видов блоков похож, но они принципиально отличаются назначением и решаемыми задачами. Поэтому вопрос, какой из них выбрать, не стоит: чтение нижеследующего материала, надеемся, убедит вас в пользе обоих.
Блок try..except
Для реакции на конкретный тип ситуации применяется блок try..except. Синтаксис его следующий:
try
<Оператор>
 <Оператор>
...
except
on EExceptionl do < Оператор обработки ИС типа EExceptionl >;
on EException2 do < Оператор >;
...
else { }
<0ператор> {обработчик прочих ИС}
  end;
Выполнение блока начинается с секции try. При отсутствии исключительных ситуаций только она и выполняется. Секция except получает управление в случае возникновения ИС. После обработки происходит выход из защищенного блока, и управление обратно в секцию try не передается; выполняются операторы, стоящие после end.
Если вы хотите обработать любую ИС одинаково, независимо от ее класса, вы можете писать код прямо между операторами except и end. Но если обработка отличается, здесь можно применять набор директив on. .do, определяющих реакцию приложения на определенную ситуацию. Каждая директива связывает ситуацию (on...), заданную своим именем класса, с группой операторов (do...).

U := 220.0; 
R := 0;
 try
I := U / R; 
except
on EZeroDivide do MessageBox('Короткое замыкание!');
  end;
В этом примере замена if. .then на try. .except, может быть, не дала очевидной экономии кода. Однако если при решении, допустим, вычислительной задачи проверять на возможное деление на ноль приходится не один, а множество раз, то выигрыш от нового подхода неоспорим — достаточно одного блока try. .except на все вычисления.
При возникновении ИС директивы просматриваются последовательно, в порядке их описания. Каждый тип исключительной ситуации, описанный после ключевого слова 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 местами, и все придет в норму.
Если возникла ситуация, не определенная ни в одной из директив, выполняются те операторы, которые стоят после else. Если и их нет, то ИС считается не обработанной и будет передана на следующий уровень обработки. Этим следующим уровнем может быть другой оператор try..except, который содержит в себе данный блок.
Примечание 
Мы детально обсудили, что и как помещают в блок try. .except. Но в ряде случаев можно... не размещать там ничего. Пустой блок применяют, когда хотят просто проигнорировать возникновение некоторых ситуаций. Скажем, у вас в программе предусмотрена некая обработка данных, которая может завершиться неудачно, но это не повлияет на дальнейшую работу, и пользователь может об этом не знать. В этой ситуации вполне уместно изолировать ее в пустом блоке try..except. Важно только не поместить туда больше кода, чем нужно — иначе "с водой можно выплеснуть и ребенка".
Если вы не предусмотрели блоков обработки ИС в своем коде, это не должно привести к аварийному завершению всего приложения. Все места в VCL, где управление передается коду разработчика (в том числе, конечно, все обработчики событий всех компонентов), заключены в такие блоки. Но, увы, в Borland не знают о конкретных проблемах вашей программы, и максимум, что они могут сделать для вас, — это проинформировать о типе и месте возникновения ИС. Стандартная обработка подразумевает вывод на экран панели текстового сообщения (из свойства Exception.Message) с указанием типа ошибки. Можно получить и развернутую информацию с именем модуля и адреса, где она имела место (2).
procedure ShowException(ExceptObject: TObject; ExceptAddr: Pointer);
имеющуюся в модуле SYSUTILS.PAS.
Если предусмотренной вами обработки ИС недостаточно, то можно продолжить ее дальше программно при помощи оператора raise.
Этот оператор уже встречался нам при описании создания пользовательских ИС. Там за ним следовал вызов конструктора ИС. Здесь же конструктор опущен: возбуждаться будет уже существующий объект ИС, приведший нас в блок:
...
sl:= TStringList. Create;
try
s1.LoadFromFile(AFileName); 
except
sl.Free; 
raise;
 end;
...
В этом примере в случае возникновения исключительной ситуации созданный список строк должен быть уничтожен. Сама же обработка предоставляется "вышестоящим инстанциям".
 
Блок try...finally
Параллельно с блоком try..except в языке существует и try. .finally. Он соответствует случаю, когда необходимо возвратить выделенные программе ресурсы даже в случае аварийной ситуации. Синтаксис блока try..finally таков:
try
<Оператор> 
<Оператор>
...
finally 
<Оператор>
...
end;
Смысл этой конструкции можно описать одним предложением: операторы, стоящие после finally, выполняются всегда.
Следующие за try операторы исполняются в обычном порядке. Если за это время не возникло никаких ИС, далее следуют те операторы, которые стоят после finally. В случае, если между try и finally произошла ИС, управление немедленно передается на операторы после finally, которые называются кодом очистки. Допустим, вы поместили после try операторы, которые должны выделить вам ресурсы системы (дескрипторы блоков памяти, файлов, контекстов устройств и т. п.). Тогда операторы, освобождающие их, следует поместить после finally, и ресурсы будут освобождены в любом случае. Блок try...finally, как можно догадаться, еще называется блоком защиты ресурсов.
Важно обратить внимание на такой факт: данная конструкция ничего не делает с самим объектом — исключительной ситуацией. Задача try...finally — только прореагировать на факт нештатного поведения программы и проделать определенные действия. Сама же ИС продолжает "путешествие" и вопрос ее обработки остается на повестке дня.
Блоки защиты ресурсов и обработчики ИС, как и другие блоки, могут быть вложенными. В этом простейшем примере каждый вид ресурсов системы защищается в отдельном блоке:
try
AllocatelstResource;
 try
Allocate2ndResource;
 SolveProblem;
 finally
Free2ndResource; 
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: j=0'); 
end;
 except
On EDivByZero do
ShowMessage('Вариант 2: i=0');
 end;
  end;
Но все же идеально правильный случай — это сочетание блоков двух типов. В один из них помещается общее (освобождение ресурсов в finally), в другой — особенное (конкретная реакция внутри except).
 
Использование исключительных ситуаций
Если произошла ошибка и возбуждена исключительная ситуация, то она будет обрабатываться по такому алгоритму:
1. Если ситуация возникла внутри блока try..except, то там она и будет обработана. Если ИС "продвинута" дальше при помощи оператора raise, а также если она возникла в блоке try. .finally, обработка продолжается.
2. Если программистом определен обработчик события Application.onException, то он получит управление. Обработчик объявлен следующим образом:
TExceptionEvent = procedure (Sender: TObject; E: Exception) of object;
3. Если программист никак не определил реакцию на ИС, то будет вызван стандартный метод showException, который сообщит о классе и месте возникновения исключительной ситуации.
Пункты 2 и 3 реализуются в методе TAppiication.HandieException. Собственно, выглядят они следующим образом:
if not (ExceptObject is EAbort) then
 if Assigned(FOnException) then
  FOnException(Sender, Exception(ExceptObject))
else
  ShcwExceptior. (Exception(ExceptObject));
Обработчик onExceptiоn нужен, если требуется выполнять одно и то же действие в любой исключительной ситуации, возникшей в вашем приложении. К примеру, назвать себя, указать координаты для обращения или предупредить, что это еще бета-версия.
program Project!;
uses 
Forms,
SysUtils, //добавлено вручную — там описан класс Exception Dialogs, 
Unitl in 'Unitl.pas' {Forml};
{$R *.RES}
type
TExceptClass = class 
public
 procedure GlobalExceptionHandler(Sender: TObject; E:Exception);
end;
procedure TExceptClass.GlobalExceptionHandler(Sender: TObject;
E:Exception); 
begin
ShowMessage('Произошла исключительная ситуация ' + E.ClassName
+ ': ' + E.Message
+ #13#10'Свяжитесь с разработчиками по тел. 222-33-44');
end; 
begin
with TExceptClass.Create do
 begin
Application.OnException := GlobalExceptionHandler;
Application.Initialize;
Application.CreateFormfTForml, Forml);
Application.Run;
Free; 
end;
  end.
Здесь класс TExceptClass создается только для того, чтобы быть носителем метода GiobaiException. Обработчик любого события — метод, и он должен относиться к какому-либо объекту. Поскольку он здесь нужен еще до инициализации форм приложения и других его составных частей, то и объект класса TExceptClass создается первым. Теперь пользователь знает, что благодарить за неожиданности нужно по указанному в сообщении об ошибке телефону разработчиков.
Примечание 
Есть и более простой способ присвоить обработчик событию Application.OnException. Для этого поместите на форму компонент типа TApplicationEvents (страница Additional Палитры компонентов), роль которого — предоставление "визуального" доступа к свойствам невизуального объекта TApplication. Среди его событий есть и OnException.
Но как "пощупать" переданный при исключительной ситуации объект? Обычная конструкция
on EExceptionType do...
указывает на класс объекта, но не на конкретный экземпляр. Если во время обработки требуется доступ к свойствам этого экземпляра, его нужно поименовать внутри on..do, указав перед именем класса некий идентификатор:
on EZD: EZeroDivide do EZD.Message := 'Деление на ноль!';
Здесь возникшее исключение выступает под именем EZD. Можно изменить его свойства и отправить дальше:
var APtr : Pointer;
Forml : TForm; 
try
APtr := Forml;
with TObject(APtr) as TBitmap do; 
except
on EZD: EInvalidCast do EZD.Message :=. EZD.Message + 'xa-xa!';
Raise;{ теперь обработка будет сделана в другом месте }
 end;
Но как поименовать исключительную ситуацию, не попавшую ни в одну из директив on..do? Или, может быть, в вашем обработчике вообще нет on. .do, а поработать с объектом надо? Описанный выше путь здесь не подходит. Для этих случаев есть пара системных функций 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(Mow)+' . ');
ExceptionErrorMessage
 (ExceptObject,ExceptAddr,@buf[StrLenfbuf)],
SizeOf(Buf)-StrLen(buf));
StrCat(Buf,#13#10);
 fs.WriteBuffer (Buf, StrLer. ;buf) ) ;
 fs.Free;
 end;
procedure TForml.ButtonlClick(Sender: TObject);
var x,y,z: real;
begin
try
try
x:=1.0;y:=0.0;
z := x/y;
except
LogException;
 raise;
end; 
except
on E:EIntError do ShowMessage('IntError');
on E:EMathError do ShowMessage('MathError');
 end;
 end;
Здесь задачу записи информации об ИС решает процедура LogException. Она открывает файловый поток и пишет туда информацию, отформатированную При помощи уже упоминавшейся функции ExceptionErrorMessage.
В качестве ее параметров выступают значения функций Exceptobject и ExceptAddr. К сформированной строке добавляется время возникновения ИС. Для каждого защищаемого блока кода создаются две вложенные конструкции try. .except. Первая, внутренняя — для вас; в ней ИС протоколируется и продвигается дальше. Внешняя — для пользователя; именно в ней проводится анализ типа ИС и готовится сообщение.
В Object Pascal существует и расширенный вариант употребления оператора
raise:
raise окземпляр объекта типа Exception> [at <адрес>]
Естественно, объектный тип должен быть порожден от Exception. To, что в таком типе ничего не переопределено, не столь важно — главное, что в обработчике ИС можно отследить именно этот тип.
ELoginError = class (Exception);
If LoginAttemptsNo > MaxAttempts then raise ELoginError.Create('Ошибка регистрации пользователя');
Конструкция at <адрес> используется для того, чтобы изменить адрес, к которому привязывается возникшая ИС, в пределах одного блока обработки ИС.
 
Коды ошибок в исключительных ситуациях
Если ваше приложение уже готовится к продаже, если вы планируете его техническую поддержку, то пора задуматься о присвоении числовых кодов
Ошибкам, возникающим в нем. Сообщение типа "Exception EZeroDivide in module MyNiceProgram at addr $0781BABO" годится для разработчика, пользователя же оно повергнет в полный ступор. Если он позвонит в вашу службу техподдержки, то, скорее всего, не сможет ничего объяснить. Гораздо грамотнее дать ему уже "разжеванную" информацию и, в том числе, числовой код.
Один из путей решения этой проблемы — размещение сообщений об ошибках в ресурсах программы. Если же вы еще делаете и несколько национальных версий программы на разных языках, то этот путь — единственный.
"Классический" способ поместить текст в файл ресурсов — 3-этапный:
1. Создается исходный файл ресурсов с расширением гс, в который помещаются необходимые строки с нужными номерами.
2. Файл обрабатывается компилятором ресурсов brcc32.exe (находится в папке bin в структуре папок Delphi). На выходе образуется одноименный файл с расширением res.
3. Файл включается в программу указанием директивы $R, например
{$R mystrings.res}.
Чтобы совместно использовать константы-номера ошибок в файле ресурсов и в коде на Delphi, вынесем их в отдельный включаемый файл с расширением inc:
const
IOError = 1000;
 FileOpenError = IOError + 1; 
FileSaveError = IOError + 2; 
InternetError = 2000;
NoConnecticnError = InternetError + 1; 
ConnectionAbortedError = InternetError + 2;
Взглянув на файл, вы увидите, что ошибки в нем сгруппированы по категориям. Советуем вам поступить так же, разделив константы категорий промежутком в 1000 или даже 10 000.
Сам файл ресурсов может выглядеть так:
#include "strids.inc" STRINGTABLE
{
FileOpenError, "File Open Error"
FileSaveError, "File Save Error"
NoConnectionError, "No Connection"
ConnectionAbortedError, "Connection Aborted"
}
"Вытащить" строку из ресурсов можно несколькими способами, но самый простой из них — просто по числовому идентификатору, переданному в функцию Loadstr (модуль SysUtils). Код
ShowMessage(LoadStr(NoConnectionError) ) ;
покажет сообщение "NO connection".
Если же строка используется при возбуждении ИС, то место идентификатору—в перекрываемом конструкторе Exception.createRes, один из вариантов которого работает подобно функции Loadstr:
if FileOpent'c:\myfile.txt", fmOpenRead) = INVALID_HANDLE_VALUE then 
raise EMyException.CreateRes(FileOpenError) ;
Таким образом, решена половина проблемы: возможным исключительным ситуациям присвоены номера, им в соответствие поставлен текст. Теперь о второй половине — как в обработчике ИС этот номер использовать.
Ясно, что нужно объявить свой класс ИС, включающий в себя свойство-код
ошибки.
EExceptionWithCode = class(Exception)
 private
 FErrCode : Integer;
public
constructor CreateResCode(ResStringRec: PResStringRec);
property ErrCode: Integer read FErrCode write FErrCode;
 end;
Тогда любой обработчик сможет к нему обратиться:
if E is EExceptionWithCode then
ShowMessage('Error code: ' + IntToStr(EExceptionWithCode(E).ErrCode) +
#13*10
+ 'Error text: ' + E.Message);
Присвоить свойству ErrCode значение можно двумя способами:
1. Добавить к классу ИС еще один конструктор, содержащий код в качестве дополнительного параметра:
constructor EExceptionWithCode.CreateResCode(Ident: Integer); 
begin
FErrCode := Ident;
inherited CreateRes(Ident);
 end;
2. Присвоить значение свойства в промежутке между созданием объекта ИС и его возбуждением:
var E: EExceptionWithCode; begin
E := EExceptionWithCode.CreateRes(NoConnectionError);
E.ErrCode := NoConnectionError;
Raise E;
 end;
Вот, казалось бы, последний штрих. Но как быть тем, кто заранее не заготовил файл ресурсов, а работает со строками, описанными в PAS-файлах? Если вы используете оператор resourcestring, то помочь вам можно.
Начнем с рассмотрения ключевого слова resourcestring. Вслед за ним описываются текстовые константы. Но, в отличие от ключевого слова const, эти константы размещаются не в сегменте данных программы, а в ресурсах, и подгружаются оттуда по мере необходимости. Каждая такая константа воспринимается и обрабатывается как обычная строка. Но за каждой из них на самом деле стоит такая структура:
PResStringRec = ^TResStringRec;
 TResStringRec = packed record 
Module: ^Cardinal;
 Identifier: Integer;
 end;
Если вы еще раз посмотрите на список конструкторов объекта Exception, вы увидите, что те из них, которые работают с ресурсами, имеют перегружаемую версию с параметром типа pResstringRec. Вы угадали правильно: они — для строк из resourcestring. А взглянув на приведенную выше структуру, вы увидите в ней поле identifier. Это то, что нам надо.
Чтобы у программиста, пользующегося resourcestring, голова не болела об уникальных идентификаторах ресурсных строк, среда Delphi берет на себя заботу об этом. Номера назначаются компилятором, начиная от 65 535 (SmallInt (-D) и ниже (если рассматривать номер как тип (SmallInt, то выше): 65 534, 65 533 и т. п. Сначала в этом списке идут несколько сотен resourcestring-констант, описанных в VCL (из модулей, чье имя заканчивается на const или consts: sysconst, DBConsts и т. п.). Затем очередь доходит до пользовательских констант (3).
С одной стороны, отсутствие лишних забот — это большой плюс; с другой стороны, разработчик не может задать строкам те номера, какие хочет.
Все остальное почти ничем не отличается от работы с "самодельными" ресурсами. Так выглядит перегружаемая версия конструктора нашего объекта EExceptionWithCode:
constructor EExceptionWithCode.CreateResCode(ResStringRec:
PResStringRec);
begin
FErrCode := ResStringRec^.Identifier;
inherited CreateRes(ResStringRec); 
end;
А так — возбуждение самой ИС:
resourcestring sErrorl = 'Error  1';
Raise EExceptionWithCode.CreateResCode
(PResStringRec(@sErrorl));
 Результат обработки показан на 3.
 
Исключительная ситуация EAbort
Если вы внимательно просмотрели код системной процедуры HandieException, то увидели там упоминание класса EAbort. ИС EAbort служит единственным — и очень важным — исключением из правил обработки. Она называется "тихой" (Silent) и отличается тем, что для нее обработка по умолчанию не предусматривает вывода сообщений на экран. Естественно, все сказанное касается и порожденных от нее дочерних объектных классов.
Применение EAbort оправдано во многих случаях. Вот один из примеров. Пусть разрабатывается некоторая прикладная программа или некоторое семейство объектов, не связанное с VCL. Если в них возникает ИС, то нужно как-то известить об этом пользователя. А между тем прямой вызов для этого функции showMessage или даже MessageBox не всегда оправдан. Для маленькой и компактной динамической библиотеки не нужно тащить за собой громаду VCL. С другой стороны, в большом и разнородном проекте нельзя давать каждому объекту или подпрограмме самой общаться с пользователем. Если их разрабатывают разные люди, такой проект может превратиться в вавилонскую башню. Тут и поможет EAbort. Эта исключительная ситуация не создается системой — ее должен создавать и обслуживать программист.
Применение EAbort — реальная альтернатива многочисленным конструкциям if..then и тем более (упаси боже!) goto. Эта ИС не должна подменять собой другие, вроде ошибки выделения памяти или чтения из файла. Она нужна, если вы сами видите, что сложились определенные условия и пора менять логику работы программы.
If LogicalCondition then Raise EAbort.Create('Condition 1');
Если не нужно определять сообщение, можно создать EAbort и проще — вызвав процедуру Abort (без параметров), содержащуюся в модуле SYSUTILS.PAS.
Функция Assert
Эта процедура и сопутствующая ей ИС EAssertionFailed специально перенесены в Object Pascal из языка С для удобства отладки. Синтаксис ее прост:
procedure Assert(expr : Boolean [; const msg: string]);
При вызове функции проверяется, чему равно значение переданного в нее булевого выражения ехрr. Если оно равно True, то ровным счетом ничего не происходит. Если же оно равно False, создается ИС EAssertionFailed. Все это было бы довольно тривиально с точки зрения уже изученного, если бы не два обстоятельства:
1. Предопределенный обработчик EAssertior.Failed устроен таким образом, что выдает не шестнадцатеричный адрес ошибки, а имя файла с исходным текстом и номер строки, где произошла ИС, как показано на 4.
2. При помощи специальной директивы компилятора {$ASSERTIONS ON/OFF} (или, что то же самое, {$с+}/{$с-}) возникновение этих ИС можно централизованно запретить. То есть в отлаживаемом коде в режиме {$с+} можно расставить вызов Assert во всех сомнительных и проверяемых местах. Когда же придет время генерации конечного варианта кода, переключением директивы на {$c-} весь отладочный вывод запрещается.
 
Резюме
Любое созданное в Delphi приложение должно обеспечивать обработку возможных исключительных ситуаций. Пусть вначале вам покажется ненужным создавать дополнительный код обработки ошибок для простейших программ, в которых вероятность ошибки минимальна. Зато впоследствии приобретенные навыки позволят вам существенно повысить надежность реальных приложений.
В Delphi для обработки исключительных ситуаций используются специальные конструкции языка Object Pascal и классы на основе базового класса исключительных ситуаций Exception.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика