Интересно, на сколько читатель уверен в своих знаниях подводных камней в работе с исключениями в Delphi. Я решил подготовить три наиболее часто встречаемых ошибки в рабочем проекте. Надо признаться, некоторые моменты меня удивили. В том числе и ответ на вопрос "в какой момент удаляются объекты исключений".
Каждый может проверить свои познания подводных камней. В листинге ниже приведены три метода Error#, каждый из которых ведет к определенной проблеме.
Пояснения я приведу вслед за кодом.
В методе Error2 мы поймали исключение, хотим отформатировать его текст и пробросить дальше. Однако это приведет к проблеме.
В Error3 мы вместо пойманного исключения стандартного класса создаем собственное исключение, которое может более точно сообщить о проблеме при обработке в вызывающем коде. Нюанс заключается в том, что исходный объект исключения получаем через AcquireExceptionObject. Данный метод может оказаться незаменим, например если мы хотим передать исключение из одного потока в другой. AcquireExceptionObject возлагает на нас ответственность за дальнейшее освобождения памяти полученного объекта исключения, а Delphi тем временем "умывает руки".
Справка Delphi сообщает, что за методом AcquireExceptionObject должен следовать ReleaseExceptionObject который уменьшает счетчик ссылок на фрейм исключения(структура в rtl описывающая исключение). Получается, что в Error3 мы забыли вызвать метод ReleaseExceptionObject? Нет. Вызов ReleaseExceptionObject нам ничем не поможет: объект утечет в любом случае.
В действительности счет ссылок и ReleaseExceptionObject актуальны только для Linux. В модуле System есть объявление типа TRaiseFrame - фрейма исключений, в случае компиляции под Windows счетчик ссылок не предусмотрен. Вот текст метода:
Метод AcquireExceptionObject "забывает" ссылку на объект исключения во фрейме исключения. А при стандартном попытке удаление исключения ничего не произойдет, так как предусмотрена проверка на nil в деструкторе объекта:
Именно по этому вызов из проблемы которая описана выше не приведет к AV:
except
А для метода test3 наиболее правильным будет решение отказаться от AcquireExceptionObject, и получать объект используя синтаксис On E: Exception do. Но все же, если удобнее использовать AcquireExceptionObject, то за ним должен следовать Raise Argument; либо явный вызов деструктора:
Более подробно, про то, что справка иногда обманывает qc.embarcadero.com
Каждый может проверить свои познания подводных камней. В листинге ниже приведены три метода 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), а дальше возбуждается исходное внешнее исключение которое ничего не знает про наш новый текст сообщения.
Более подробно можно найти описание на stackoverflow.
Ошибка 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;" подразумевает что мы пытаемся возбудить другой объект исключения, а не тот, что находится сейчас на вершине стека возбужденных исключений. Для этого вычищаем объект исключения, что находится на вершине стека и вталкиваем в стек его новый объект. Проблема заключается в том, что удаляемый и вталкиваемый объект являются одним и тем же объектом.
Единственное место, где в документации Embarcadero явно говориться, что так делать нельзя нашел в статье Handling exceptions in Delphi.
Правильно выполнить вызов 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
Подводя итог, как делать нельзя:
//AVOn E: Exception do raise E;
//Утечка E := AcquireExceptionObject; raise EMyException.Create(E.Message);
//Утечка E := AcquireExceptionObject; try Foo(E); finally ReleaseExceptionObject(E); //Вызов метода бессмысленнен end;
Не очень понятно, почему FormatException находится в "вспомогательных", если ошибка второго и третьего примеров сидит именно в ней.
ОтветитьУдалитьУ автора этой функции явно есть проблемы с пониманием отношений владелец-ресурс. В одном случае функция создаёт новый ресурс и передаёт право на него вызывающему, успешно забывая про аргумент. В другом случае она не создаёт ресурсов и работает только с уже созданным объектом (которым уже кто-то владеет). В итоге на выходе функции будет или объект, который нужно освобождать, или объект, который не нужно освобождать.
Это - багодром, вне зависимости от того, что именно делает функция - работает ли она с исключениями или чем угодно ещё. У вызывающего нет никакого способа узнать, передали ли ему объект, который нужно освобождать, или объект, которым уже кто-то владеет и поэтому ему, наоборот, не нужно его освобождать. А если он не может об этом узнать, то нет возможности написать его корректно.
Ну а поведение raise достаточно очевидно:
1. raise Exception.Create('Error Message');
Здесь мы создаём объект исключения, но нигде его не освобождаем. Т.е. мы передали право на него в RTL. Привычно? Очевидно? Понятно? Вроде да.
2. on E: Exception do ShowMessage(E.Message);
Здесь мы используем переданный нам объект, но не освобождаем его, т.к. владелец не мы. Привычно? Очевидно? Понятно? Вроде да.
Соединяем один и два - и получаем, что:
on E: Exception do raise E; - явная ошибка, т.к. мы в raise передали объект, права на владение которым у нас нет. Иными словами, E и так уже освобождается RTL, так мы ещё раз говорим: вот тебе объект, ты его удали.
Тут не надо никакой особой документации, чтобы увидеть ошибку.
Так что суммарно тут неочевиден только первый пример. И то - если не знать, что такое исключения (на уровне ОС) и что "raise;" перевозбуждает именно оригинальное исключение, а не обёртку.
Убрал комментарий про "вспомогательные" методы, что бы больше никого не смущать
УдалитьАлександр, подпишусь под каждым вашим словом. Все вполне очевидно, когда знаешь с чем работаешь. Примеры были безусловно искусственны и показывают, как до чего может довести работа с исключениями, когда в функции получаем объект, без знания кто за него отвечает.
УдалитьУверен, что для многих исключения это черный ящик, который "как то сам" работает и сам следит за своим временем жизни. Одна из причин, это то, что в борланде хотели все "упростить", предоставив простой инструмент, не сообщив о том, как легко им прострелить себе ногу. И если признаться, то недавно я и сам думал, что освобождение объекта исключения происходит только на выходе из блока except..end.