Управление ошибками, или обработка исключений в Delphi
[ Андрей Чудин ]
ж. Программист, №08 / 2002 г.
Понятие исключений
Исключение, или исключительная ситуация, возникает тогда, когда нормальное выполнение приложения прерывается ошибкой или другим событием. В этом случае управление приложением передается в обработчик исключения.
Под обработкой исключений понимают стандартные методы для обнаружения и обработки необычных, непредвиденных и исключительных состояний или событий. Она предоставляет формальный способ отклонить поток управления функции на участок кода, готового принять контроль над данной исключительной ситуацией и известить об этом пользователя или принять соответствующие меры.
Конечно, возможно отслеживание ошибок и критических ситуаций без использования механизма обработки исключений, однако сложность и ненадежность такого подхода свидетельствуют не в его пользу.
Синтаксис обработки
Итак, для того, чтобы в полной мере использовать всю мощь механизма исключений, рекомендуется:
- использовать блоки try..except и try..finally;
- локализовывать с помощью специальных методов место ошибки.
Локализация места ошибки возможна с помощью трех подходов к обработке ошибок.
- использование процедуры Assert;
- использование адреса ошибки и MAP-файла с информацией об адресах процедур;
- использование повторного вызова исключения в обработчике try … except … end с отображением GUID.
Использование процедуры Assert
В Object Pascal введена специализированная процедура Assert, назначение которой - помощь в отладке кода и контроле над выполнением программы. Процедура является аналогом макроса ASSERT, который широко применяется практически во всех программах, написанных с использованием C и C++ и их библиотек. Синтаксис процедуры Assert:
procedure Assert(expr : Boolean [; const msg: string]);
Процедура Assert обычно применяется в следующих случаях:
- в начале процедуры или функции для проверки правильности переданных аргументов;
- в конце работы алгоритма для проверки правильности работы алгоритма;
- для проверки правильности выполнения "надежных" функций, то есть тех функций, которые всегда должны выполняться успешно всегда, и их невыполнение рассматривается как фатальная ошибка программы; хороший пример - функция CloseHandle вызываемая с верным дескриптором: в правильности выполнения этой функции можно практически не сомневаться, однако результат ее выполнения все-таки можно и нужно проверить.
Директива {$ASSERTIONS ON|OFF} позволяет компилировать вызовы assert или игнорировать их. Таким образом, в отладочной версии директива будет включена и компилятор будет генерировать код для вызовов assert, а в готовую версию этот код можно не включать, установив директиву {$ASSERTIONS OFF}.
Использование адреса ошибки и MAP-файла с информацией об адресах процедур
MAP-файл представляет собой список сегментов в скомпилированной программе с указанием типа сегментов и их расположения в готовом коде. Т. к. при возникновении исключения можно получить адрес источника исключения, анализ MAP-файла позволит найти, в какой процедуре находится вызвавший сбой код. Ручной анализ MAP-файла неудобен, однако возможно написание специального анализатора для него. Это направление выходит за рамки представленного здесь материала. Дополнительно о структуре MAP-файла можно прочитать на странице http://www.siteBuilder.ru/borlandMapFile.htm.
Использование повторного вызова исключения в обработчике try … except … end
В обработчике исключения можно вызвать его возобновление при помощи ключевого слова raise. При этом можно обеспечить передачу ему уникального кода, однозначно идентифицирующего место ошибки.
try
...
except
on E: Exception do
raise Exception.Create(E.Message + #13#10#13#10 +
'{17676641-B7C3-11D4-AAE7-000000000000}');
end
В приведенном примере строка уникального номера сгенерирована Delphi по нажатию клавиш Ctrl+Shift+G и представляет собой GUID (Globally Unique Identifier), уникальность которого гарантирована.
Разработка компонента для обработки ошибок
Остановимся на последнем методе локализации и реализуем протоколирование информации обо всех сбоях в программе в файл для последующей локализации сбоев и анализа источника ошибок в исходном коде. Предлагается разработать компонент, реализующий обработку исключений и ведение - насколько это возможно - подробного протокола исключений. Нас интересуют класс исключения, текст сообщения об ошибке и, желательно, GUID ошибки. Дата и время исключения, имя пользователя могут оказаться нелишними. Также пригодятся данные о конфигурации клиентской машины.
Таким образом, цель разработки компонента состоит в ведении подробного протокола всех возникающих в программе исключений. Протокол ведется в локальном файле.
Основная функция компонента состоит в том, что он устанавливает на себя обработчик исключительных ситуаций, а также обработчик процедуры Assert. Компонент может быть настроен на использование файла протокола ошибок, в котором будет сохраняться вся доступная информация о происходящих исключениях. После обработки (протоколирования) возникающего исключения компонент эскалирует (raise) ее по стеку вызовов.
Рассмотрим методы установки собственных обработчиков исключений.
Перехват сообщений об исключительных ситуациях
Обработчик исключений в приложении можно переопределить через Application.OnException. Этот обработчик вызывается приложением при исключении и имеет следующий тип:
type TExceptionEvent = procedure (Sender: TObject; E: Exception) of object;
После установки обработчика Application.OnException обработку всех исключений можно представить диаграммой последовательности, приведенной на рис. 1.
Обработчик процедуры Assert
Обработка вызовов Assert производится переопределением указателя System.AssertErrorProc. Процедура обработчика должна иметь следующий синтаксис:
procedure AssertErrorHandler
(const Message, Filename: string; LineNumber: Integer; ErrorAddr: Pointer);
Назначив этим обработчикам наши процедуры обработки, мы получим контроль над потоком исключений и вызовов Assert.
Использование компонента (назовем его TglExceptionHandler) можно проиллюстрировать несколькими примерами:
Пример 1 Вызов Assert будет обработан и сохранен в протокол,
т.к. TglExceptionHandler перехватывает вызовы Assert:
{$ASSERTIONS ON}
procedure LoadSettings(FileStream: TFileStream);
begin
assert(Assigned(FileStream), 'Потоковый объект должен быть создан ранее');
end;
Пример 2 Вызов Assert будет обработан и сохранен в протокол с текстом исключения и GUID:
try
x := StrToInt(sSomeStringParameter);
except
on E: Exception do Assert(Assigned(E), E.Message +
'{1CC90D41-B651-11D4-AAE3-000000000000}');
end;
Пример 3 Возобновление исключения напрямую через компонент.
Исключение будет обработано и сохранено в протокол,
пользователь увидит понятное сообщение об ошибке:
eh: TglExceptionHandler;
...
try
ConvertDataToXML(); // некий вызов
except
on E: Exception do eh.Raise_(E, 'Ошибка вызова конвертера XML',
'{1CC90D41-B651-11D4-AAE3-000000000000}');
end;
end;
Вариант использования, приведенный во втором примере, выглядит очень неплохо, так как включает в себя больший набор информации для диагностирования ошибки и возвращает номер строки в исходном коде. Есть только одно неудобство: пользователь программы получает техническое сообщение об ошибке, как правило, на английском языке. Да и исподники постоянно меняются, номер строки и имя модуля - ненадежная информация. Третий вариант не включает информации о строке в исходном коде, но GUID позволяет однозначно локализовать источник ошибки и отображает понятное для пользователя сообщение, что очень важно.
Известно, что если пользователь не понимает, что происходит, то он начинает сильно нервничать. Мы, как разработчики, в этом смысле должны по возможности беречь покой пользователя, так как в конечном итоге это и наш покой. Отсюда делаем вывод, что третий вариант следует признать наилучшим (рис. 2).
Резюме правил обработки исключений с помощью класса TglExceptionHandler
Использование
Компонент класса TglExceptionHandler следует выносить на главную форму или на форму, создаваемую в первую очередь. Можно, конечно, добавить его на любую форму, но в этом случае до создания этой формы ошибки обрабатываться не будут. При добавлении компонента в DataModule в его обработчике OnCreate нужно добавить строку glExceptionHandler.Refresh;.
У компонента есть два перегруженных метода протоколирования и возобновления ошибки Raise_:
procedure Raise_(E: Exception; const Message, ID: string);
procedure Raise_(E: Exception; ID: string);
где
- E - объект исключения;
- Message - сообщение для пользователя;
- ID - уникальный идентификатор источника ошибки (GUID); генерируется в Delphi IDE нажатием Ctrl+Shift+G; ID позволит однозначно идентифицировать место ошибки.
В случае использования первого варианта вызова пользователь увидит сообщение Message, а в файл протокола будет записана подробная информация + ID. На основании ID можно будет однозначно найти место ошибки.
В случае использования второго синтаксиса вызова пользователь увидит то же самое, что он увидел бы при отсутствии обработчика + ID. В файл протокола также будет записана подробная информация + ID.
Первый вариант следует использовать для участков кода, когда критическая ситуация прогнозируема. Например, конвертация введенной текстовой строки в число. В этом случае пользователь получит понятное ему сообщение (ведь он не обязан знать технический английский) и введет корректные данные. Возникает вопрос: зачем протоколировать такие сбои, как ввод неверных данных? Быть может и не стоит, можно упростить обработку, но статистика ошибок пользователя, которая может быть потом получена из протокола - тоже ценная информация.
Второй вариант рекомендуется использовать для всех (или почти всех) процедур и обработчиков. В этом случае пользователь программы получит кроме малопонятного ему сообщения об ошибке еще и код, по которому программист сможет быстро найти источник ошибки.
Пример использования:
... some code ...
EH: TglExceptionHandler;
... some code ...
try
Database.Connected := true; //...критический участок кода
except
on E:Exception do EH.Raise_(E, 'Не удается соединиться с сервером' + Database.DatabaseName,
'{013D4094-D745-11D4-BBFB-0080C86E0E20}')
end;
... some code ...
procedure YForm1.sbOkButton_Click(Sender: TObject);
try
... some code ...
Edit.Text := spSomeStoredProc.FlieldByName('поля с этим именем может и не быть!').AsString;
... some code ...
except
on E:Exception do EH.Raise_(E, '{013D4094-D745-11D4-BBFB-0080C86E0E67}')
end;
Если следовать приведенным выше рекомендациям, то число сообщений на непонятном техническом английском, получаемых пользователем, будет сведено к минимуму, а разработчики программы смогут быстро и однозначно идентифицировать место ошибки по уникальному коду.
В библиотеке Globus.lib рассмотренный компонент обработки исключений умеет создавать лог ошибок, записывая в него конфигурацию клиентской системы, протоколировать ошибки, отправлять уведомления через MailSlot и даже создавать скриншоты экрана приложения при сбое (для особо тяжелых случаев). Также компонент может очень быть очень удобен для ведения протокола работы программы. Стоит ли говорить, что код компонента снабжен подробнейшими комментариями, которые ответят на вопросы, не нашедшие отражения в материале этой статьи.
Протокол обработки выглядит так:
exception log
- заголовок
ExeModule= C:\ Projects\Spell\Source\spell.exe
OSPlatform= NT
CPUKind= 15
CPUName= P15
TotalPhys= 267894784
AvailPhys= 32600064
TotalPageFile= 646012928
AvailPageFile= 395452416
TotalVirtual= 2147352576
AvailVirtual= 2017599488
ColorDepth= 32
SystemFont= SmallFont
VRefreshRate= 85
GraphicResolution= 1152x864
- протокол
Computer= SATELLITE
User= arakh
Datetime= 04.04.2002 17:50:20
ID= patch cmdAdd_PackQuantity upplied
Exception class= EDatabaseError
ExceptAddr= 004A51EC
Computer= SAN
User= chudin
Datetime= 26.06.2002 19:21:41
ExceptionMessage= tblClients: Не могу выполнить эту операцию для закрытого набора данных (dataset)
ID= {B6EE9D6D-5C34-4F09-884D-E0620566C192}
Exception class= EUpdateClientsError
ExceptAddr= 004A5245
Computer= SAN
User= chudin
Datetime= 26.06.2002 20:31:00
ID= {FBB12FD2-F2D3-11D5-BCAF-0080C86E0E20}
ExceptionMessage= Поле 'INN' не может быть изменено
...
Преимущества описанного подхода
Самое важное, чего удается достичь с использованием описанного подхода к обработке исключений - это явное упрощение локализации ошибок в уже распространенных конечным пользователям программных продуктах. Также программа ведет себя более дружественно к пользователю при сбоях, т.к. для наиболее критических участков кода мы позаботились о выводе пользователю понятного сообщения на родном языке.
Рассмотренный подход к обработке ошибок применяется автором статьи и рядом других разработчиков уже более двух лет и использован более чем в десятке программных проектов.
Функциональность работы с удаленной обработкой ошибок от клиентских приложений в реальном времени прекрасно рассмотрена в статье Ильдара Даутова на сайте "Королевство Дельфи" (http://www.delphikingdom.com/mastering/errors.htm). Рекомендуем прочитать этот очень полезный материал, он - первоисточник такого подхода.
Компонент TglExceptionHandler входит в состав библиотеки Globus VCL Extention Library. Эта библиотека создается автором этой статьи начиная с 1998 года и распространяется бесплатно с открытыми исходными кодами. Содержит около 30 визуальных и 20 невизуальных компонентов. Краткую информацию о библиотеке, а также ее дистрибутив можно найти тут: http://www.siteBuilder.ru/GlobusLib.htm
Вадим Беркович разработал эксперт для Delphi, который позволяет работать с протоколом ошибок непосредственно из среды разработки и автоматически локализовывать ошибки по их GUID. Код эксперта для Delphi 5 можно загрузить здесь: http://www.siteBuilder.ru/ExceptionViewerIDE.htm.
Автор выражает сердечную благодарность Марии Сысойкиной и Анне Рахманиной за моральную поддержку и многочисленные замечания касательно содержания и формы статьи.
Литература
- Р. Конопка. Создание оригинальных компонент в среде Delphi, 1996 год
- Тейксейра С., Пачеко К. Delphi 5. Руководство разработчика. В 2 т. Т. 2. Разработка компонентов и работа с базами данных, 2000 год.
- Win32 SDK Reference
Весь материал, размещенный на сайте www.bookresearch.ru, является собственностью авторов соответствующих материалов.
Любая перепечатка и перенос материалов на другие сайты возможны только с разрешения авторов и администратора сайта.
Любой может предложить свой материал для публикации у нас. Пишите администратору сайта.
|