Интересно, на сколько читатель уверен в своих знаниях подводных камней в работе с исключениями в Delphi. Я решил подготовить три наиболее часто встречаемых ошибки в рабочем проекте. Надо признаться, некоторые моменты меня удивили. В том числе и ответ на вопрос "в какой момент удаляются объекты исключений".
Каждый может проверить свои познания подводных камней. В листинге ниже приведены три метода Error#, каждый из которых ведет к определенной проблеме.
Пояснения я приведу вслед за кодом.
procedure RaiseOSException;
var
res: integer;
zero: integer;
begin
zero := 0;
res := 5 div zero;
end;
function FormatException(E:Exception): Exception;
begin
if not (E is EMyException) then
result := EMyException.Create(E.Message)
else
begin
E.message := Format('Ошибка обработки исключения: %s', [E.Message]);
result := E;
endl
end;
procedure Error1;
begin
try
RaiseOSException;
except
on E: Exception do
begin
E.Message := Format('Заворачиваем в свой текст: %s', [E.Message]);
Raise;
end;
end;
end;
procedure Error2;
begin
try
raise EMyException.Create('Alarm!!111');
except
on E: Exception do
begin
Raise FormatException(E);
end;
end;
end;
procedure Error3;
begin
try
raise Exception.Create('Alarm!!111');;
except
Raise FormatException(AcquireExceptionObject);
end;
end;
Ответы:
- Error1 - потеря изменений в сообщение исключения;
- Error2 - Ошибка Access Violation
- Error3 - Утечка памяти
Разберем каждый случай более детально:
Потеря сообщений
В методе Error1 мы изменяем текст исходного сообщения и возбуждаем исключение вновь. Но в диалоге исключения мы не увидим текста с "Заворачиваем в свой текст: %s".
При возбуждение внешнего исключения Delphi заворачивает его в обертку - собственное исключения и дает обработать в блоке except..end. Далее, в вызове Raise вызывается System._RaiseAgain в котором происходит удаление объекта делфового исключения(в котором мы и сделали изменение данных поля Message), а дальше возбуждается исходное внешнее исключение которое ничего не знает про наш новый текст сообщения.
Ошибка Access Violation (Или "в какой момент удаляются объекты исключений")
В методе Error2 мы поймали исключение, хотим отформатировать его текст и пробросить дальше. Однако это приведет к проблеме.
Проблема случая из метода Error2 заключается в том, что Delphi попытается дважды удалить один и тот же объект исключения. Первый раз Raise FormatException(E), второй раз скорее всего где нибудь в TApplication, в завершения цикла обработки исключений. Ответ находится в System._HandleAnyException в которой каждый сможет найти комментарий:
{ we come here if an exception handler has thrown yet another exception }
{ we need to destroy the exception object and pop the raise list. }
Синтаксис "Raise Argument: Exception;" подразумевает что мы пытаемся возбудить другой объект исключения, а не тот, что находится сейчас на вершине стека возбужденных исключений. Для этого вычищаем объект исключения, что находится на вершине стека и вталкиваем в стек его новый объект. Проблема заключается в том, что удаляемый и вталкиваемый объект являются одним и тем же объектом.
Правильно выполнить вызов raise без аргументов("Raise;"), либо использовать метод AcquireExceptionObject. О нем ниже.
Утечка памяти
В Error3 мы вместо пойманного исключения стандартного класса создаем собственное исключение, которое может более точно сообщить о проблеме при обработке в вызывающем коде. Нюанс заключается в том, что исходный объект исключения получаем через AcquireExceptionObject. Данный метод может оказаться незаменим, например если мы хотим передать исключение из одного потока в другой. AcquireExceptionObject возлагает на нас ответственность за дальнейшее освобождения памяти полученного объекта исключения, а Delphi тем временем "умывает руки".
Справка Delphi сообщает, что за методом AcquireExceptionObject должен следовать ReleaseExceptionObject который уменьшает счетчик ссылок на фрейм исключения(структура в rtl описывающая исключение). Получается, что в Error3 мы забыли вызвать метод ReleaseExceptionObject? Нет. Вызов ReleaseExceptionObject нам ничем не поможет: объект утечет в любом случае.
В действительности счет ссылок и ReleaseExceptionObject актуальны только для Linux. В модуле System есть объявление типа TRaiseFrame - фрейма исключений, в случае компиляции под Windows счетчик ссылок не предусмотрен. Вот текст метода:
function AcquireExceptionObject: Pointer;
begin
if RaiseListPtr <> nil then
begin
Result := PRaiseFrame(RaiseListPtr)^.ExceptObject;
PRaiseFrame(RaiseListPtr)^.ExceptObject := nil;
end
else
Result := nil;
end;
Метод AcquireExceptionObject "забывает" ссылку на объект исключения во фрейме исключения. А при стандартном попытке удаление исключения ничего не произойдет, так как предусмотрена проверка на nil в деструкторе объекта:
procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;
Именно по этому вызов из проблемы которая описана выше не приведет к AV:
except
E := AcquireExceptionObject;
Raise E; // В данном случае это правильный код
end;
А для метода test3 наиболее правильным будет решение отказаться от AcquireExceptionObject, и получать объект используя синтаксис On E: Exception do. Но все же, если удобнее использовать AcquireExceptionObject, то за ним должен следовать Raise Argument; либо явный вызов деструктора:
procedure Error3;
var
e : Exception;
begin
try
raise Exception.Create('Alarm!!111');;
except
e := AcquireExceptionObject;
try
Raise FormatException(e);
finally
FreeAndNil(e);
end;
end;
end;
Более подробно, про то, что справка иногда обманывает
qc.embarcadero.com
Подводя итог, как делать нельзя:
//AV
On E: Exception do
raise E;
//Утечка
E := AcquireExceptionObject;
raise EMyException.Create(E.Message);
//Утечка
E := AcquireExceptionObject;
try
Foo(E);
finally
ReleaseExceptionObject(E); //Вызов метода бессмысленнен
end;