Если вы попробуете делать там слишком много - у вас могут быть неприятности.
К примеру, если ваш деструктор передаёт ссылки на себя другим функциям, то эти функции могут решить вызывать ваши
IUnknown._AddRef
и IUnknown._Release
во время своей работы. Посмотрите на этот код:
function TMyObject._Release: Integer; begin Result := InterlockedDecrement(FRefCount); if Result = 0 then Destroy; end; destructor TMyObject.Destroy; begin if FNeedSave then Save; inherited; end;Это не выглядит очень уж страшным, не так ли? Объект просто сохраняет себя перед разрушением.
Но метод
Save
мог бы выглядеть примерно так:
function TMyObject.Save: HRESULT; var spstm: IStream; spows: IObjectWithSite; begin Result := GetSaveStream(spstm); if SUCCEEDED(hr) then begin Supports(spstm, IObjectWithSite, spows); if Assigned(spows) then spows.SetSite(Self); Result := SaveToStream(spstm); if Assigned(spows) then spows.SetSite(nil); end; end;Сам по себе он выглядит нормально. Мы получаем поток (stream) и сохраняем себя в него, дополнительно устанавливая сведения о контексте (site) - на случай если потоку нужна будет дополнительная информация.
Но этот простой код в сочетании с тем фактом, что он запущен из деструктора, даёт нам рецепт для катастрофы. Посмотрите что при этом происходит:
- Метод
_Release
уменьшает счётчик ссылок до нуля и выполняет удалениеSelf
. - Деструктор пытается сохранить объект.
- Метод
Save
получает поток для сохранения и устанавливаетSelf
в качестве контекста. Это увелививает счётчик ссылок с нуля до единицы. - Метод
SaveToStream
сохраняет объект в поток. - Метод
Save
очищает контекст потока. Это приводит к уменьшению счётчика ссылок нашего объекта с единицы до нуля. - Поэтому метод
_Release
вызывает деструктор объекта второй раз.
Поэтому, как минимум, вы должны вставить
Assert
в ваш метод AddRef
, чтобы гарантировать, что вы не увеличиваете счётчик ссылок с нуля:
function TMyObject._AddRef: Integer; begin Assert(FRefCount > 0); Result := InterlockedIncrement(FRefCount); end;Это поможет вам легко отлавливать "случаи загадочного двойного вызова деструктора объекта". Но когда вы идентифицируете проблему, то что же вам с ней делать? Мы поговорим об этом в следующий раз.
Примечание переводчика:
Сказанное здесь в точности применимо и к объектам, реализующим интерфейсы в Delphi (просто потому, что интерфейсы COM - это единственные интерфейсы в Delphi). Однако в Delphi объект может существовать, даже когда его счётчик равен 0. Например:
var MyObj: TMyObject; begin MyObj := TMyObject.Create; FreeAndNil(MyObj); end;В этом примере счётчик ссылок будет равен 1 во время работы конструктора и опустится до 0 после выхода из Create, после чего счётчик будет равен 0 до конца жизни объекта.
Сравните это с:
var MyObj: IMyObject; begin MyObj := TMyObject.Create; MyObj := nil; end;В этом примере счётчик ссылок будет равен 1 во время работы конструктора и опустится до 0 после выхода из Create, после чего снова поднимется до 1 из-за копирования интерфейса в переменную
MyObj
.С учётом этого момента код выше нужно немного изменить. Например, так:
function TMyObject._AddRef: Integer; begin Assert(FRefCount >= 0); Result := InterlockedIncrement(FRefCount); end; function TMyObject._Release: Integer; begin Result := InterlockedDecrement(FRefCount); if Result = 0 then Destroy; end; procedure TMyObject.BeforeDestruction; begin if RefCount <> 0 then System.Error(reInvalidPtr); FRefCount := -1; end;См. также этот запрос в QC.
В вызове TMyObject._Release не нужно добавлять строку FRefCount := -1, она уже добавлена в BeforeDestruction, который будет вызван при вызове Destroy.
ОтветитьУдалитьСпасибо, исправил.
ОтветитьУдалить