1. Исключения в конструкторе
Если в конструкторе любого объекта произойдет исключение, будет автоматически вызван деструктор этого объекта. Это обеспечивается "магией" компилятора. Отсюда следует, что правильной идиомой для создания и уничтожения класса является:Obj = TSomeClass.Create; try // ... работа с Obj finally Obj.Free; end;Т.о. если исключение произойдет в конструкторе, деструктор будет вызван автоматически, а если после завершения конструктора - то деструктор будет вызван через Free из блока finally.
Строго говоря, не является ошибочным и следующий вариант:
Obj = nil; try Obj = TSomeClass.Create; // ... работа с Obj finally Obj.Free; end;Однако, такой способ некрасив и избыточен (лишнее присвоение Obj = nil). К тому же, помещение конструктора в блок try-finally само по себе вовсе не помогает против исключения в конструкторе. В самом деле, посмотрим внимательнее на строчку "Obj = TSomeClass.Create;". Здесь сначала выполняется конструктор, а потом его результат присваевается переменной Obj. Если в конструкторе произойдет исключение, то до присвоения переменной дело просто не дойдет. Поэтому в блоке finally переменная Obj по-прежнему будет nil. Так что только автоматический вызов деструктора может предотвратить потерю ресурсов при исключении в конструкторе. Вероятно поэтому создатели Delphi его и сделали. Сложно предложить другой способ.
Другое важное следствие - деструктор должен быть готов к работе с не до конца созданным объектом. Например, рассмотрим следующий класс:
type TExample = class private FList : TList; // ... public constructor Create; destructor Destroy; override; // ... end; constructor TExample.Create; begin inherited; // ... иницилизация других private-полей FList := TList.Create; end; destructor TExample.Destroy; var I : Integer; begin for I := 0 to FList.Count - 1 do // что-то сделать с FList[I] FList.Free; inherited; end;Деструктор этого класса содержит потенциальную опасность. Если исключение произойдет в конструкторе до того, как FList был создан, то деструктор будет вызван автоматически, и при этом FList будет все еще nil. Т.о. обращение FList.Count вызовет access violation, которе "замаскирует" изначальное исключение, так что сложно будет понять, что именно случилось не так. Правильнее было написать:
destructor TExample.Destroy; var I : Integer; begin if FList <> nil then begin for I := 0 to FList.Count - 1 do // что-то сделать с FList[I] FList.Free; end; inherited; end;
AfterConstruction и BeforeDestruction
Теперь вспомним, что все классы в Delphi имеют два унаследованных от TObject "магических" метода AfterConstruction и BeforeDestruction. Первый из них вызывается сразу после конструктора, второй - непосредственно перед деструктором. Интересно отметить, что если исключение произойдет в AfterConstruction, деструктор будет вызван автоматически точно также, как если бы исключение произошло бы в конструкторе. Еще более интересно то, что при возникновении исключения в конструкторе ни AfterConstruction ни BeforeDestruction не вызываются (при возникновении исключения в AfterConstruction метод BeforeDestruction тоже не вызывается). Можно только догадываться, почему Delphi ведет себя именно так, но, по-видимому, вызывать AfterConstruction в таком случае представлялось разработчикам Delphi нелогичным - раз "Construction" не завершено, то и "AfterConstruction" быть не должно. А вызов BeforeDestruction при том, что AfterConstruction не сработал, привел бы к ряду проблем. Например, для классов, унаследованных от TInterfacedObject. Дело в том, что InterfacedObject.InitInstance устанавливает счетчик ссылок RefCount а 1, а TInterfacedObject.AfterConstruction уменьшает RefCount на 1. Делается это для того, чтобы в процессе работы конструктора объект вдруг не уничтожился, это если RefCount увеличится, а потом уменьшится до 0 (В help'e пишут по этому поводу: "When an instance of TInterfacedObject is allocated, RefCount is incremented to prevent its destruction while processing the constructor. AfterConstruction decrements that RefCount to re-establish the correct reference count after all constructors have executed."). Так что, если в конструкторе такого объекта произойдет исключение, то при вызове деструктора RefCount будет равен 1. Если бы в этот момент сработал BeforeDestruction, чей код имеет видif RefCount <> 0 then Error(reInvalidPtr);то мы получили бы run-time error "Invalid pointer operation" вместо "нормального" исключения.
Так или иначе, BeforeDestruction вызван не будет. Помимо упомянутого обхода потенциальной проблемы с TInterfacedObject, это имеет еще такие последствия (которые иногда полезно иметь в виду)
- Если объект унаследован от TCustomForm или TDataModule, то обработчик события OnDestroy вызван не будет (что логично, т.к. OnCreate тоже не вызывался)
- Если объект унаследован от TComponent, то при вызове деструктора в ComponentState не будет флага csDestroying
2. Исключения в OnCreate и OnDestroy
Классы, порожденные от TCustomForm и TDataModule имеют события OnCreate и OnDestroy. События эти вызываются из методов DoCreate и DoDestroy этих классов. Эти методы перехватывают любое исключение, которые могло бы произойти в обработчиках этих событий и обрабатывают его с помощью Application.HandleException. Таким образом, если в событии OnCreate вашей формы произойдет исключение, оно будет тут же обработано через Application.HandleException, конструктор не будет прерван, и форма будет создана успешно (т.е. автоматического вызова деструктора, описанного в предыдущем пункте не произойдет). Насколько форма будет при этом функциональна - другой вопрос. Поэтому если при создании формы нужно проделать какую-то критическую операцию - т.е. такую операцию, что при ее неудаче форме вообще нет смысла создаваться - то надо делать ее в конструкторе, а не в событии OnCreate.3. Исключения в initialization и finalization
Если исключение произойдет в секции initialization или finalization какого-либо из модулей, то оно вообще не будет обработано и программа завершится аварийно с run-time error 217. Казалось бы, исключение должно быть обработано с помощью ExceptProc из SysUtils, но почему-то этого не происходит.4. Исключения при создании COM-объектов
Исключения при создании COM-объектов связаны с еще большим количеством проблем, чем исключения при создании других объектов. Как известно, если метод COM-объекта объявлен как safecall, то компилятор Delphi неявно "заворачивает" его в блок try-except, причем в блоке except вызывается метод SafeCallException, который гасит исключение, зато обставляет все таким образом, чтобы клиент, вызывающий данный метод, мог восстановить исключение. Таким образом исключение передаются практически "прозрачно" от COM-сервера к его клиенту ("практически", потому что класс исключения у клиента будет уже не тем, что на сервере, а всегда EOleException).К сожалению, все это не работает при создании COM-объекта. Когда клиент создает COM-сервер (с помощью CreateCOMObject или чего-то аналотичного), COM-объект создается своей фабрикой классов. Если в конструкторе COM-объекта или его методе Initialize произойдет исключение, оно будет перехвачено в методе фабрики классов TComObjectFactory.CreateInstanceLic. При этом, если свойство фабрики классов ShowErrors имеет значение True (а это значение по умолчанию), сервер выдаст MessageBox с текстом ошибки. У клиента же вызов CreateCOMObject завершится маловразумительной ошибкой "Критический сбой" (код E_UNEXPECTED). Все это еще ничего, если сервер является inprocess (DLL) или локальным. Если же сервер вызывается удаленно (через DCOM), то скорей всего он вообще не имеет доступа к экрану серверного компьютера, так что показанный из TComObjectFactory.CreateInstanceLic MessageBox с текстом ошибки не будет виден и нажать на нем "ОК" будет невозможно. В результате сервер намертво зависнет. Даже если серверный процесс имеет доступ к экрану (что будет иметь место, если на сервере Windows 95/98, или же DCOM сконфигурирован соответствующим образом), то все равно сообщение об ошибке на экране сервера мало поможет клиенту. Выводы из этого такие:
- Если предполагается использование COM-сервера через DCOM, то необходимо выставлять ShowErrors в False у всех фабрик классов.
- Конструктор и метод Initialize COM-объектов не должны вызывать исключения. Все равно клиент не поймет в чем дело.
- Потенциально опасные действия делать в отдельном методе, который клиент вызывает сразу после создания объекта
- Обрамить потенциально опасные действия блоком try-except, причем в except погасить исключение и выставить некое свойство "инициализация неуспешна". Клиент в таком случае сразу после создания объекта должен проверить это свойство.
12 comments:
Почему на блоге так мало тем про кризис, Вас этот вопрос не волнует?
Супер статья! Подписался на RSS, буду следить =)
Даже и не придирешься!
Так зачитался, что пропустил бы любимую передачу ;)
Интересно стало, а комментарии которые не нравятся автору здесь удаляют?
привет. Админ, хошь прикол ? :D к тебе на сайт можна заходить через яндекс
спасибо за статью… добавил в ридер
Заметил такую тенденцию, что в блогах появилось много не адекватных комментариев, не могу понять, это что кто то спамит так? А зачем, чтоб падлу комуто сделать))) Имхо глупо…
Хорошая статья, узнал много нового!)
настоящая проблема для нашего времени, с нетерпением буду ждать продолжения Ваших рассуждений на по этому вопросу.
Да таков наш современный мир и боюсь с этим ни чего невозможно поделать:)
Очень хороший пост! Спасибо за проделанную работу!
Post a Comment