Подводные камни при работе с исключениями в Delphi: Raise Exception, AcquireExceptionObject, исключения операционной системы
Каждый может проверить свои познания подводных камней. В листинге ниже приведены три метода 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 — Утечка памяти
Потеря сообщений
Ошибка Access Violation (Или «в какой момент удаляются объекты исключений»)
В методе Error2 мы поймали исключение, хотим отформатировать его текст и пробросить дальше. Однако это приведет к проблеме.
{ we come here if an exception handler has thrown yet another exception }
{ we need to destroy the exception object and pop the raise list. }
Утечка памяти
В 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;