Во второй форме напишем обработчик события OnClose
таким образом, чтобы он устанавливал по закрытию действие caFree
. Добавим поле строкового типа, перекроем конструктор и метод WndProc
так, чтобы окончательный код выглядел следующим образом (листинг 3.52, пример CloseAV на компакт- диске).
TForm2
type
TForm2 = class(TForm)
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
S: string;
protected
procedure WndProc(var Message: TMessage); override;
public
constructor Create(AOwner: TComponent); override;
end;
….
constructor TForm2.Create(AOwner: TComponent);
begin
S:= 'abc';
inherited;
end;
procedure TForm2.WndProc(var Message: TMessage);
begin
inherited;
S[2]:= 'x'; { * }
end;
procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action:= caFree;
end;
Обратите внимание, что в конструкторе сначала присваивается значение полю S
, и лишь потом вызывается унаследованный конструктор. Это сделано потому, что по умолчанию S
содержит пустую строку, т. е. nil
, а уже при вызове унаследованного конструктора окно получит сообщения, для обработки которых будет вызван метод WndProc
. Если в этот момент S
будет по-прежнему nil
, попытка обратиться ко второму символу строки вызовет Access violation. Поэтому еще до начала работы унаследованного конструктора поле S
должно получить подходящее значение.
Запустим программу и попытаемся закрыть второе окно. Возникнет исключение Access Violation: Write of address 00000001. Проблема будет в строке, отмеченной {*}
. При этом любые другие манипуляции с окном никаких исключений вызывать не будут.
При Action = caFree
после завершения работы метода FormClose VCL вызывает метод TCustomForm.Release
. Проблема именно в нем: если попытаться закрыть Form2
с помощью Release
, возникнет то же самое исключение. В справке Release
позиционируется как безопасный способ удаления формы из ее собственного метода. К сожалению, в действительности это не так: реализация этого удаления оставляет желать лучшего и может приводить к попыткам работать с объектом тогда, когда его уже не существует.
При вызове Release
в очередь помещается сообщение CM_RELEASE
, адресатом которого является сама удаляемая форма. В очередном цикле петли сообщений CM_RELEASE
извлекается из очереди и передается на обработку. Так как сообщение адресовано форме, она же его и обрабатывает. Рассмотрим более подробно, как это происходит. (Детально механизм обработки сообщений в VCL описан в CM_RELEASE
.)
Система передает управление оконной процедуре. Для каждого экземпляра визуального компонента VCL создает свою оконную процедуру с помощью MakeObjectInstance
. Эта процедура вызывает метод объекта MainWndProc
, передающий управление тому методу, на который указывает свойство WindowProc
. По умолчанию это WndProc
. WndProc
не обрабатывает CM_RELEASE
самостоятельно, а передает его методу Dispatch
. Dispatch
пытается найти для этого сообщения специальный обработчик (метод с директивой message
) и, т. к. в TCustomForm
такой обработчик описан (он называется CMRelease
), передаёт управление ему.
И здесь начинается самое интересное. CMRealease
просто вызывает Free
, удаляя тем самым объект, т. е. объект удаляется из метода самого объекта, что делать запрещено. Таким образом, после выполнения Free
управление вновь получает CMRealease
. Из него управление возвращается в Dispatch
, оттуда — в WndProc
, затем — в MainWndProc
, далее — в оконную процедуру, и только после этого управление получает код, который никак не связан с конкретным экземпляром компонента. Мы видим, что после обработки CM_RELEASE
и удаления объекта его методы продолжают работать. Методы уже не существующего объекта!
В принципе, методы несуществующего объекта могут вполне нормально завершить свою работу, если не будут обращаться к его полям или иным образом использовать указатель Self
, который к этому моменту будет уже недействительным. Но стоило нам только вставить в один из этих методов код, задействующий поле объекта, как возникла ошибка.