Главная страница
    Top.Mail.Ru    Яндекс.Метрика
Форум: "Основная";
Текущий архив: 2002.11.11;
Скачать: [xml.tar.bz2];

Вниз

Стиль: Вызов виртуальных методов из деструкторов   Найти похожие ветки 

 
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;
Скачать: [xml.tar.bz2];

Наверх





Память: 0.57 MB
Время: 0.008 c
1-34072
Davy
2002-10-30 09:34
2002.11.11
Сообщение о владельце прог-м. продуктом


1-33941
IGray
2002-11-01 03:26
2002.11.11
Гамма-коррекция ЯРКОСТИ (а не КОНТРАСТА) нужна..


3-33885
Alexxx9801
2002-10-21 15:45
2002.11.11
TClientDataSet и RefreshRecord


14-34237
Peter Gluhiy
2002-10-23 13:50
2002.11.11
Для любителей OnLine тестов!


7-34271
John Eagle
2002-09-09 17:20
2002.11.11
убить поток





Afrikaans Albanian Arabic Armenian Azerbaijani Basque Belarusian Bulgarian Catalan Chinese (Simplified) Chinese (Traditional) Croatian Czech Danish Dutch English Estonian Filipino Finnish French
Galician Georgian German Greek Haitian Creole Hebrew Hindi Hungarian Icelandic Indonesian Irish Italian Japanese Korean Latvian Lithuanian Macedonian Malay Maltese Norwegian
Persian Polish Portuguese Romanian Russian Serbian Slovak Slovenian Spanish Swahili Swedish Thai Turkish Ukrainian Urdu Vietnamese Welsh Yiddish Bengali Bosnian
Cebuano Esperanto Gujarati Hausa Hmong Igbo Javanese Kannada Khmer Lao Latin Maori Marathi Mongolian Nepali Punjabi Somali Tamil Telugu Yoruba
Zulu
Английский Французский Немецкий Итальянский Португальский Русский Испанский