Текущий архив: 2002.11.11;
Скачать: CL | DM;
ВнизСтиль: Вызов виртуальных методов из деструкторов Найти похожие ветки
← →
Igorek (2002-10-24 13:45) [0]Поясню на примере: есть класс A, от него наследуется класс B. В классе А есть виртуальная ф-ция f, которая переопределена в насленике В. Причем переопределенный вариант работает с полями, которые есть в классе В. А в деструкторе класса А она вызывается. Теперь представим уничтожение класса В. Сначала очищаются поля в своем классе, потом вызывается деструктор предка (и через него функция f соответственно). И поскольку она виртуальная, то вызовется переопределенный вариант из В, который обратиться к уже очищенным полям из В.
Варианты выхода из ситуации:
1) избегать прямого или косвенного вызова виртуальных функций из деструкторов вообще (а для этого иногда дублировать код в деструкторах)
2) давать этим функциям знать, не из деструктора ли они вызываются (ввести в класс флажок Destroing или добавить флажок параметр CalledFromDestructor)
3) разделять функциональность ф-ции f на ту, которая выполняется всегда, и на ту, которая выполняется только при вызове не из деструктора; т.е. еще две функции, первую из которых и вызывать из деструктора; но может случиться, что f придется разделить более чем на две части (если они идут непоочередно).
Вот такой код на Паскале:
A = class
procedure f; virtual;
destructor Destroy; virtual;
end;
B = class(A)
procedure f; override;
destructor Destroy; override;
end;
destructor A.Destroy;
begin
f;
end;
destructor B.Destroy;
begin
inherited;
end;
Если компилировать и трассировать на Делфи 6, то увидим, что при уничтожении обьекта класса В, порядок вызовов будет такой
-В.Destroy
-A.Destroy
-B.f
Народ, как вы считаете, какой выход лучше всего? И не является ли приметой плохого стиля необходимость вызова виртуальных методов из деструкторов вообще?
← →
Skier (2002-10-24 13:51) [1]>Igorek
Вообще-то вот это очень нехорошая штука.
НЕТ ВЫЗОВА ДЕСТРУКТОРА КЛАССА-ПРЕДКА TObject
С этого стоит начать копать дальше...
destructor A.Destroy;
begin
f;
end;
И кроме того Компилятор обязательно выдаст Warning на эту
строку :
destructor Destroy; virtual;
← →
VaS (2002-10-24 13:52) [2]Во-первых:
A = class
procedure f; virtual;
destructor Destroy; override;
end;
А вызывать виртуальные функции из деструктора конечно можно. Деструктор в OP - не совсем то же, что и в С++. Воспринимай его как событие OnDestructing (но не OnDestructed) :)
← →
Igorek (2002-10-24 14:08) [3]
> Skier © (24.10.02 13:51)
> >Igorek
> Вообще-то вот это очень нехорошая штука.
> НЕТ ВЫЗОВА ДЕСТРУКТОРА КЛАССА-ПРЕДКА TObject
> С этого стоит начать копать дальше...
>
> destructor A.Destroy;
> begin
> f;
> end;
>
> И кроме того Компилятор обязательно выдаст Warning на эту
> строку :
> destructor Destroy; virtual;
Вы правы в обеих случаях, но это не важно для данного случая. Написал так, с ходу.
> А вызывать виртуальные функции из деструктора конечно можно.
> Деструктор в OP - не совсем то же, что и в С++. Воспринимай
> его как событие OnDestructing (но не OnDestructed) :)
Недопонял. Событие OnDestructing (BeginDestructing) должно выставить флажок внутри класса (UnderDestructing)?
← →
Igorek (2002-10-25 10:46) [4]Просто поднимаю ветку. Ну-ну, будут еще мнения?
← →
Skier (2002-10-25 10:52) [5]>Igorek
> И поскольку она виртуальная, то вызовется переопределенный
> вариант из В, который обратиться к уже очищенным полям из
> В.
Почему ?
← →
VaS (2002-10-25 11:29) [6]А так?
A = class
procedure f; virtual;
destructor Destroy; override;
end;
B = class(A)
procedure f; override;
destructor Destroy; override;
end;
destructor A.Destroy;
begin
inherited;
f;
end;
destructor B.Destroy;
begin
inherited;
end;
← →
VaS (2002-10-25 11:44) [7]Немного не так :) Вот, например, имеем в В TList, с которым работаем в виртуальной f() и уничтожаем в деструкторе:
A = class
procedure f; virtual;
destructor Destroy; override;
end;
B = class(A)
private
l: TList;
public
procedure f; override;
constructor Create;
destructor Destroy; override;
end;
{ A }
destructor A.Destroy;
begin
f;
inherited;
end;
procedure A.f;
begin
end;
{ B }
constructor B.Create;
begin
l:=TList.Create;
end;
destructor B.Destroy;
begin
inherited;
l.Free;
end;
procedure B.f;
begin
inherited;
l.Clear;
end;
end.
Т.е. вызываем inherited Destroy до очистки специфичных для дочернего класса данных (полей).
← →
Юрий Зотов (2002-10-25 12:17) [8]1. Вызов виртуальных (как и любых других) методов из деструктора - это нормальная практика, и примеров тому полно в VCL. Просто надо проследить за правильным порядком вызовов, вот и все (ведь никто не заставляет вызывать inherited строго последним, или строго первым, да и вообще этот вызов не обязателен).
2. Если класс компонентский, то Флажок csDestroying уже есть в ComponentState.
← →
Ученик (2002-10-25 12:31) [9]>Юрий Зотов © (25.10.02 12:17)
"...да и вообще этот вызов не обязателен)."
Что Вы имеете ввиду ?
← →
Юрий Зотов (2002-10-25 12:38) [10]То, что написал - вызов inherited не является строго обязательным, даже и в деструкторе.
← →
Ученик (2002-10-25 12:41) [11]>Юрий Зотов © (25.10.02 12:38)
Так можно ?
type
TA = class
private
FP : PChar;
public
constructor Create;
destructor Destroy; override;
end;
TB = class(TA)
destructor Destroy; override;
end;
constructor TA.Create;
begin
inherited Create;
GetMem(FP, 1000);
end;
destructor TA.Destroy;
begin
FreeMem(FP);
inherited Destroy;
end;
destructor TB.Destroy;
begin
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
TB.Create.Free
end;
← →
Толик (2002-10-25 12:47) [12]to Ученик © (25.10.02 12:41)
Так можно, но не нужно. :) Очевидно, что память теряется.
А вот насчёт того, что в виртуальных ф-ях inherited не обязателен Юрий Зотов © (25.10.02 12:17) абсолютно прав. Попробуйте объявить в родительском классе ф-ю, объявленную как virtual abstract, а затем в дочернем классе вызвать inherited. Вызывать inherited или нет это сугубо личное дело: если надо - вызываете, если не надо - то нет...
← →
Ученик (2002-10-25 12:50) [13]>Толик © (25.10.02 12:47)
Поэтому, я и попросил Юрия Зотова пояснить, что он имеет ввиду :-)
← →
vuk (2002-10-25 12:50) [14]to VaS:
>Деструктор в OP - не совсем то же, что и в С++. Воспринимай его
>как событие OnDestructing (но не OnDestructed) :)
Почти, но не совсем так. Как OnDestructing лучше воспринимать метод BeforeDestruction, т.к. в этом методе еще можно отменить разрушение экземпляра (если сгенерировать исключение), а вот если уж попали в деструктор, то обратного пути уже нет.
to Ученик:
>Что Вы имеете ввиду ?
Если в деструкторах предков не производится никаких действий, освобождающих ресурсы или изменяющих какие-либо глобальные переменные, говоря проще, деструкторы предков всех предков пусты, то их вызов необязателен. К примеру, не обязателен вызов деструктора, если предком является TObject. Правда, дело в том, что зачастую невозможно узнать что делается в деструкторе предков, поэтому лучше явно делать вызов деструктора предка. Дополнительно это увеличивает совместимость кода наследника с возможными будущими изменениями в коде предка.
← →
Юрий Зотов (2002-10-25 18:58) [15]> Толик © (25.10.02 12:47)
И в невиртуальных тоже.
> Ученик © (25.10.02 12:41)
Замечательный пример. Правда, ничто не мешает гарантированно освободить память в другом методе, и тогда деструктор становится вообще не нужен. Ни inherited, ни даже собственный.
Вот еще один пример, более явный. И без всяких деструкторов.
procedure KillMem;
var
P: pointer;
begin
GetMem(P, ...)
end;
Так тоже можно. Но тоже не нужно. И подобных примеров - море. Как говорится, если уж человек написал ТАКОЕ, то это надолго. Хоть с деструктором, хоть без него.
Я говорил, что вызывать inherited не обязательно. И это так. Но разве я говорил, что не обязательно ДУМАТЬ?
← →
Ученик (2002-10-25 19:01) [16]>Юрий Зотов © (25.10.02 18:58)
Спасибо за ответ, теперь будем надеяться, все поняли правильно :-)
← →
Anatoly Podgoretsky (2002-10-25 19:27) [17]Ученик © (25.10.02 12:41)
Неочевидно, вот если приведешь текст конструкторов тогда можно будет сказать одназначно
← →
Ученик (2002-10-25 19:29) [18]>Anatoly Podgoretsky © (25.10.02 19:27)
???
← →
Anatoly Podgoretsky (2002-10-25 19:53) [19]Вот у тебя есть
destructor TB.Destroy;
begin
end;
попробуй представить
constructor TB.Create;
begin
end;
← →
Ученик (2002-10-25 21:06) [20]>Anatoly Podgoretsky © (25.10.02 19:53)
Представил, в чем вопрос ?
← →
Юрий Зотов (2002-10-25 21:31) [21]Видимо, Анатолий имел в виду, что если в TB.Create нет вызова inherited, то вызов inherited в TB.Destroy не только не нужен, но и даже приведет к ошибке.
← →
vuk (2002-10-25 22:00) [22]>Видимо, Анатолий имел в виду, что если в TB.Create нет вызова
>inherited, то вызов inherited в TB.Destroy не только не нужен,
>но и даже приведет к ошибке.
IMHO при грамотно написанных деструкторах это безразлично. То есть если в деструкторе делается попытка освобождения какого-либо ресурса, то должна быть проверка того, что именно мы пытаемся освободить, и если не захватывали ресурсы, то и освобождать нечего.
А вот при невызове конструктора предка вероятность получить неверно работающий экземпляр будет очень велика...
← →
Юрий Зотов (2002-10-25 22:11) [23]> vuk © (25.10.02 22:00)
В подобных случаях я УМЫШЛЕННО не делаю проверок. Считаю, что пусть лучше вылетит Exception и послужит сигналом к тому, что где-то в программе что-то не так. Иначе ошибка может оказаться замаскированной, а такие ловить значительно сложнее.
← →
Anatoly Podgoretsky (2002-10-25 22:28) [24]Это к вопросу об утечке, ее в данном варианте просто не будет, но так писать конструктор и деструктор стоит только для демонстации, к замечанию, что неочевидно, вот если бы были приведены все конструкторы, тогда это можно было бы сказать однозначно, а так было неочевидно
← →
Ученик (2002-10-25 22:40) [25]>Anatoly Podgoretsky © (25.10.02 22:28)
Это был законченый пример, о каких других конструкторах идет речь ?
← →
Igorek (2002-10-29 18:37) [26]2 Юрий Зотов © (25.10.02 12:17)
Странно. Я лично привык считать, что вызовы конструкторов и деструкторов обязательны, и порядок их строго определен. Для конструкторов - от предков к наследникам, для деструкторов - наоборот. Т.е. мы обязаны писать вначале конструктора и в конце деструктора inherited.
← →
MBo (2002-10-29 18:57) [27]>вызовы... обязательны, и порядок их строго определен
>мы обязаны писать вначале конструктора и в конце деструктора inherited.
Это по меньшей мере разумно. Обращение в конструкторе с новыми, введенными нами в наследнике полями может зависеть от того, созданы ли старые. Наоборот - может потребоваться очень редко, поэтому в большинстве случаев обычный порядок правилен. Если же в конструкторе родителя ничего не делается, либо мы изменяем то,что там делалось (например, в родителе было только Height:=100, а в потомке Height:=200), нет смысла его вызывать.
← →
Igorek (2002-10-29 19:07) [28]2 MBo © (29.10.02 18:57)
> например, в родителе было только Height:=100, а в потомке
> Height:=200
Я бы предпочел делать виртуальную процедуру для инициализации InitParentClassObject. Вызывать ее один раз в конструкторе предка, а перегружать в наследниках при необходимости. В наследнике она бы уже была другая - InitInheritedClassObject, и вызывалась бы из конструктора своего класса.
Короче вариантов как всегда масса...
← →
Igorek (2002-10-29 19:11) [29]А последовательность вызова конструкторов/деструкторов зато можно сохранить. И вообще в них только память выделять/освобождать, а инициализировать поля в отдельных методах: если есть необходимость поменять порядок или функциональность - просто сделать.
← →
Юрий Зотов (2002-10-29 20:01) [30]> Igorek © (29.10.02 18:37)
Мы никому ничего не обязаны. Нужен вызов - вызывайте, не нужен - не вызывайте, ни то, ни другое не запрещено. Только сначала думайте, нужно вызывать, или не нужно, вот и все.
Посмотрите конструктор TComponent. Где там inherited? Нет его. Потому что в данном случае он не нужен. И у нас могут быть подобные варианты.
Буквально сейчас я пишу класс, в котором не вызывается унаследованный конструктор. Потому в данном классе это не требуется.
← →
Igorek (2002-10-30 09:33) [31]
> Юрий Зотов © (29.10.02 20:01)
Ну хорошо. А вот например случай такой: наследуем класс, в котором не нужно вызывать унаследованный конструктор. Потом от него класс, в котором нужно вызвать этот конструктор (теперь уже непрямого предка). Придется применять извращенные методы. Так что жесткий порядок вызовов также важен из соображений последующего наследования.
Принято считать, что констр/дестр нужны для выделения памяти под поля класса и для их инициализации. С инициализацией понятно - если в наследнике одно и то же инициализируется, то нет необходимости вызывать унаследованный вариант. Но вот с памятью? Тут уж иначе в наследнике не напишешь - память можно выделить одним способом (или в крайнем случае менять этот способ надо очень редко). Соответственно странно, когда унаследованый конструктор, в котором выделяется память не надо вызывать.
> Буквально сейчас я пишу класс, в котором не вызывается унаследованный
> конструктор. Потому в данном классе это не требуется.
Соответственно интерестно, выделяется ли в вашем унаследованном варианте память?
← →
MBo (2002-10-30 10:02) [32]>Igorek
Не пойму, какую память имеешь в виду?
Память под все поля, в том числе и унаследованные, выделится автоматически, грубо говоря, по самому факту вызова конструктора.
← →
Igorek (2002-10-30 10:18) [33]
> MBo © (30.10.02 10:02)
> >Igorek
> Не пойму, какую память имеешь в виду?
А память под обьекты, для которых нужен явный вызов их конструкторов?
← →
MBo (2002-10-30 10:21) [34]Естественно, если такие есть, надо вызывать.
В чем проблема-то? Есть необходимость - вызывай Inherited, нет - не вызывай.
← →
Юрий Зотов (2002-10-30 10:23) [35]> наследуем класс, в котором не нужно вызывать унаследованный
> конструктор. Потом от него класс, в котором нужно вызвать этот
> конструктор
Так не бывает. Сами прикиньте, как это может быть?
> А память под обьекты, для которых нужен явный вызов их
> конструкторов?
Об этом и речь. Сказано же - ДУМАТЬ надо.
← →
Igorek (2002-10-30 20:16) [36]
> Юрий Зотов © (30.10.02 10:23)
> > А память под обьекты, для которых нужен явный вызов их
> > конструкторов?
>
> Об этом и речь. Сказано же - ДУМАТЬ надо.
А зачем думать, если можно соглашение, стиль, подход выработать? ;-)
← →
Юрий Зотов (2002-10-31 01:30) [37]> Igorek
> А зачем думать, если можно соглашение, стиль, подход
> выработать?
Стиль - это хорошо. Помнится, я тут и сам не так давно выступал за грамотный и безопасный стиль. И сейчас остаюсь на той же точке зрения.
Но не нужно впадать в другую крайность и считать, что стиль есть автоматическая панацея от всего на свете. Стиль стилем, а ДУМАТЬ надо ВСЕГДА. И вот пример, если угодно. Как раз на тему вызова inherited в конструкторе.
Пусть у нас есть чья-то чужая библиотека, а в ней есть класс TSomeClass со встроенным объектом TStrings:
TSomeClass = class(TPersistent)
private
FStrings: TStrings;
procedure SetStrings(const Value: TStrings);
public
constructor Create;
destructor Destroy; override;
procedure Assign(Source: TPersistent); override;
procedure SomeProc;
published
property Strings: TStrings read FStrings write SetStrings;
end;
Его реализация стандартная:
procedure TSomeClass.SetStrings(const Value: TStrings);
begin
FStrings.Assign(Value)
end;
constructor TSomeClass.Create;
begin
inherited;
FStrings := TStringList.Create
end;
destructor TSomeClass.Destroy;
begin
FStrings.Free;
inherited
end;
procedure TSomeClass.Assign(Source: TPersistent);
begin
if Source is TSomeClass
then FStrings.Assign(TSomeClass(Source).Strings)
else inherited
end;
procedure TSomeClass.SomeProc;
begin
... // здесь что-то хитрое, ради чего и был сделан TSomeClass
end;
Далее, пусть мы написали какой-то свой TMyStringList с какими-то дополнительными возможностями:
TMyStringList = class(TStringList)
... // здесь что-то наше
end;
И теперь мы хотим написать TMyClass - потомок класса TSomeClass, у которого внутренним объектом был бы не стандартный TStringList, а наш TMyStringList. При условии, что править исходники чужой библиотеки нельзя и копировать ее код к себе тоже нельзя. То есть, надо сделать нормальное наследование, но с заменой класса внутреннего объекта. При этом обеспечив полную работоспособность нашего потомка и его полную совместимость с предком.
Попробуйте. Потом, если хотите, можем вернуться к теме.
Только не надо говорить, что эта задача надуманная - по такой технологии был написан уже не один практический компонент. Например, нестандартный TListView с расширенными возможностями.
← →
Ученик (2002-10-31 08:37) [38]type
TMyClass = class(TSomeClass)
public
constructor Create;
end;
TSomeClassWrapper = class(TPersistent)
private
FStrings: TStrings;
end;
constructor TMyClass.Create;
begin
inherited Create;
with TSomeClassWrapper(Self) do begin
FreeAndNil(FStrings);
FStrings := TMyStringList.Create
end
end;
← →
Юрий Зотов (2002-10-31 09:04) [39]Вот об этом и речь. В TMyClass.Create вызываем inherited, а потом тут же уничтожаем то единственное, что этот inherited делает (единственное - это потому, что TSomeClass - потомок TPersistent). То есть - выполняем кучу лишних операций.
Теперь представим себе программму, в которой обработка данных построена таким образом, что в каком-то длительном цикле TMyClass создается и уничтожается много раз. Куча лишних операций во столько же раз увеличивается и мы начинаем говорить, что тормозит железо, Windows - что угодно, кроме нашей программы, в общем. Хотя достаточно всего лишь изменить конструктор:
constructor TMyClass.Create;
begin
TSomeClassWrapper(Self).FStrings := TMyStringList.Create
end;
И все. И это тоже стиль, между прочим.
Так что вывод остается прежним: стиль стилем, а думать нужно всегда.
← →
Ученик (2002-10-31 09:40) [40]>Юрий Зотов © (31.10.02 09:04)
Вы согласны с тем, что человек не в состоянии запомнить все особенности написанных им классов (например, где он вызывал inherited Create(Destroy), а где нет) ?
Страницы: 1 2 вся ветка
Текущий архив: 2002.11.11;
Скачать: CL | DM;
Память: 0.57 MB
Время: 0.008 c