![]() |
| ||
|
|
Новости |
Поиск |
FAQ |
Статьи |
Книги |
Новости VCL
| | Форумы ЧАТ |
Орешник |
|
| |||
| |||
« предыдущая ветвь | форум | следующая ветвь »
Стиль: Вызов виртуальных методов из деструкторов
Igorek © (2002-10-24 13:45:00)Поясню на примере: есть класс 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
Народ, как вы считаете, какой выход лучше всего? И не является ли приметой плохого стиля необходимость вызова виртуальных методов из деструкторов вообще?
down (2002-10-31 12:59:00) [1]Спасибо!
Юрий Зотов (2002-10-31 12:51:00) [2]Например, WriteProcessMemory(..., @Strings,...), или что-то типа этого:
var
PStrings: ^TStrings;
begin
PStrings := @Strings;
PStrings^ := TMyStringList.Create
end;
Это тоже не на 100% универсально (проходит для полей с прямым доступом по чтению), но уже не зависит ни от каких смещений. А поэтому годится для любого по порядку поля и не зависит от версии предка.
down (2002-10-31 12:15:00) [3]
> Полностью согласен. Поэтому я использую более универсальные
> и более надежные способы - но они тоже не требуют вызова
> inherited. Вот о чем в данном случае идет речь, а не о том
> или ином способе подмены класса.
Юрий, а какие еще методы есть (в случае готового класса, исходники которого править нельзя)?
Юрий Зотов (2002-10-31 11:52:00) [4]> Igorek © (31.10.02 10:33)
> Я думаю, это уже полухакерские методы - приводить тип и
> пользоваться тем фактом, что у поля оказывается одинаковое
> смещение относительно начала обьекта.
Полностью согласен. Поэтому я использую более универсальные и более надежные способы - но они тоже не требуют вызова inherited. Вот о чем в данном случае идет речь, а не о том или ином способе подмены класса.
> Сам класс TSomeClass недостаточно обеспечивает такого рода
> использование.
Естественно. Он же не нами писан и его автор не рассчитывал на такое наследование. Зато он написал какой-то очень хитрый метод SomeProc, ради которого нам и нужна вся эта бодяга. Этот пример я придумал из головы, но такая же ситуация реально существует, например, в TListView. Для Items там предусмотрена CreateListItem, но попробуйте вживить в него свои колонки. И это далеко не только в TListView.
> Т.е. в данной формулировке задача не имеет корректного и
> красивого решения
Мне самому не нравятся задачи подобного рода, но иногда приходится решать и такие. Что делать...
> Igorek © (31.10.02 10:52)
> Это как прокладывать асфальт наново каждый раз в том
> направлении, куда едем.
Между прочим, в Германии таким способом ремонтируют дороги - сначала прокладывают объездную (капитальную, не грунтовку), а потом делают старую. Вероятно, это одна из причин того, что с дорожной сетью там совсем неплохо. При следующем ремонте строить уже ничего не надо, а вместо одной плохой дороги получаем две хороших.
> Программист должен по возможности думать ОДИН раз над ОДНОЙ
> задачей.
Лучше не над одной, а над ЭТОЙ и БУДУЩИМИ. Одновременно. Этим и отличается написание ядра (классов, библиотек...) от прикладного программирования. Мышление другое, понимаете? Как в Германии.
Выше я привел пример конструктора, который, с одной стороны, потом не придется переделывать (с вероятностью 5 девяток) и который, с другой стороны, не заставит меня потом думать над повышением скорострельности программы. Ни текущей, ни последующих. Вот это и называется - думать ОДИН раз. Как в Германии.
Уф-ф-ф. Устал я, ребята, от этих споров. Поступайте, как знаете. Всем до свидания.
Igorek (2002-10-31 10:52:00) [5]
> Юрий Зотов © (31.10.02 10:36)
> Позиции ясны и в главном (что всегда надо думать) мы сошлись.
Вот тут я, простите, в корне не согласен. Это как прокладывать асфальт наново каждый раз в том направлении, куда едем. Программист должен по возможности думать ОДИН раз над ОДНОЙ задачей.
Юрий Зотов (2002-10-31 10:36:00) [6]> Delphi1 - Delphi2 - Delphi3 - Delphi4 - Delphi5 - Delphi6 -
> Delphi 7
Есть ВОЗМОЖНОСТЬ (внесения изменений), а есть ее ВЕРОЯТНОСТЬ.
Вы представляете, что может произойти, если в очередной версии Borland перекроит базовые классы? Тем более, TObject и TPersistent, от которых чуть ли не вся VCL и растет. Произойдет что-то типа катастрофы, причем почти гарантированно.
Так вот - какова вероятность того, что Borland пойдет на такой шаг? Тем более, что он запросто может отразиться на популярности Delphi - а значит, и на доходах фирмы.
Мизерная. Практически нулевая.
Стоит ли закладываться на некую мифическую вероятность и ради нее жертововать реальной (!!!) эффективностью своих программ?
В D6 Borland перекроила юниты. Это была вынужденная мера, связанная с обеспечением кроссплатформенности. Один из итогов - своей цели они достигли (что весьма перспективно в смысле тех же доходов). Другой итог - даже у нас, где Delphi на 90% халявная, очень многие до сих пор не хотят уходить с D5. Потому что не хотят перекапывать свои исходники.
Что уж тогда говорить о людях, которые платят за инструмент свои кровные? Они тем более не хотят головной боли за свои же деньги.
Когда Вы врезаете новый замок, Вы делаете запасные ключи. На случай потери основных. Но Вы не делаете их в 10 экземплярах на случай 10-кратной потери, верно? Хотя такая вероятность тоже существует, но Вы понимаете, что она весьма мала и руководствуетесь здравым смыслом.
Почему же в программировании должно быть иначе?
В общем, предлагаю этот спор прекратить. Позиции ясны и в главном (что всегда надо думать) мы сошлись. А уж кто какое решение из этих раздумий принимает - это его дело.
Igorek (2002-10-31 10:33:00) [7]
> Юрий Зотов © (31.10.02 09:04)
Я думаю, это уже полухакерские методы - приводить тип и пользоваться тем фактом, что у поля оказывается одинаковое смещение относительно начала обьекта.
Сам класс TSomeClass недостаточно обеспечивает такого рода использование. Вариантов масса, как всегда. Например сделать виртуальную функцию (как CreateParams для контролов) c var параметром. Передавать в нее можно как раз FStrings. И если пользователь хочет создавать для этого поля обьект другого класса, то он ее перегружает, создает свой обьект и присваивает параметру.
Т.е. в данной формулировке задача не имеет корректного и красивого решения (imho)
Ученик (2002-10-31 10:00:00) [8]>Юрий Зотов © (31.10.02 09:51)
"...который стабильно работает и без всяких запоминаний..."
1. Согласен
2. Частично, например, пока в этом нет необходимости, никогда не задумываюсь, что делает (или что будет делать) inherited Destroy, inherited Create, поэтому можно сказать, что вызываю их механически - плохой стиль - может быть, а может быть Delphi1 - Delphi2 - Delphi3 - Delphi4 - Delphi5 - Delphi6 - Delphi 7
Юрий Зотов (2002-10-31 09:51:00) [9]Конечно. Надо просто писать такой код, который стабильно работает и без всяких запоминаний. Как в примере выше.
А Вы согласны с тем, что:
1. Думать нужно всегда.
2. Механическое следование любому стилю - это плохой стиль.
Ученик (2002-10-31 09:40:00) [10]>Юрий Зотов © (31.10.02 09:04)
Вы согласны с тем, что человек не в состоянии запомнить все особенности написанных им классов (например, где он вызывал inherited Create(Destroy), а где нет) ?
Юрий Зотов (2002-10-31 09:04:00) [11]Вот об этом и речь. В TMyClass.Create вызываем inherited, а потом тут же уничтожаем то единственное, что этот inherited делает (единственное - это потому, что TSomeClass - потомок TPersistent). То есть - выполняем кучу лишних операций.
Теперь представим себе программму, в которой обработка данных построена таким образом, что в каком-то длительном цикле TMyClass создается и уничтожается много раз. Куча лишних операций во столько же раз увеличивается и мы начинаем говорить, что тормозит железо, Windows - что угодно, кроме нашей программы, в общем. Хотя достаточно всего лишь изменить конструктор:
constructor TMyClass.Create;
begin
TSomeClassWrapper(Self).FStrings := TMyStringList.Create
end;
И все. И это тоже стиль, между прочим.
Так что вывод остается прежним: стиль стилем, а думать нужно всегда.
Ученик (2002-10-31 08:37:00) [12]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 01:30:00) [13]> 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 с расширенными возможностями.
Igorek (2002-10-30 20:16:00) [14]
> Юрий Зотов © (30.10.02 10:23)
> > А память под обьекты, для которых нужен явный вызов их
> > конструкторов?
>
> Об этом и речь. Сказано же - ДУМАТЬ надо.
А зачем думать, если можно соглашение, стиль, подход выработать? ;-)
Юрий Зотов (2002-10-30 10:23:00) [15]> наследуем класс, в котором не нужно вызывать унаследованный
> конструктор. Потом от него класс, в котором нужно вызвать этот
> конструктор
Так не бывает. Сами прикиньте, как это может быть?
> А память под обьекты, для которых нужен явный вызов их
> конструкторов?
Об этом и речь. Сказано же - ДУМАТЬ надо.
MBo (2002-10-30 10:21:00) [16]Естественно, если такие есть, надо вызывать.
В чем проблема-то? Есть необходимость - вызывай Inherited, нет - не вызывай.
Igorek (2002-10-30 10:18:00) [17]
> MBo © (30.10.02 10:02)
> >Igorek
> Не пойму, какую память имеешь в виду?
А память под обьекты, для которых нужен явный вызов их конструкторов?
MBo (2002-10-30 10:02:00) [18]>Igorek
Не пойму, какую память имеешь в виду?
Память под все поля, в том числе и унаследованные, выделится автоматически, грубо говоря, по самому факту вызова конструктора.
Igorek (2002-10-30 09:33:00) [19]
> Юрий Зотов © (29.10.02 20:01)
Ну хорошо. А вот например случай такой: наследуем класс, в котором не нужно вызывать унаследованный конструктор. Потом от него класс, в котором нужно вызвать этот конструктор (теперь уже непрямого предка). Придется применять извращенные методы. Так что жесткий порядок вызовов также важен из соображений последующего наследования.
Принято считать, что констр/дестр нужны для выделения памяти под поля класса и для их инициализации. С инициализацией понятно - если в наследнике одно и то же инициализируется, то нет необходимости вызывать унаследованный вариант. Но вот с памятью? Тут уж иначе в наследнике не напишешь - память можно выделить одним способом (или в крайнем случае менять этот способ надо очень редко). Соответственно странно, когда унаследованый конструктор, в котором выделяется память не надо вызывать.
> Буквально сейчас я пишу класс, в котором не вызывается унаследованный
> конструктор. Потому в данном классе это не требуется.
Соответственно интерестно, выделяется ли в вашем унаследованном варианте память?
Юрий Зотов (2002-10-29 20:01:00) [20]> Igorek © (29.10.02 18:37)
Мы никому ничего не обязаны. Нужен вызов - вызывайте, не нужен - не вызывайте, ни то, ни другое не запрещено. Только сначала думайте, нужно вызывать, или не нужно, вот и все.
Посмотрите конструктор TComponent. Где там inherited? Нет его. Потому что в данном случае он не нужен. И у нас могут быть подобные варианты.
Буквально сейчас я пишу класс, в котором не вызывается унаследованный конструктор. Потому в данном классе это не требуется.