Что плохого в сокрытии виртуального метода базового класса?

Я получаю предупреждения компилятора Delphi о Method 'Create' hides virtual method of base.

я рассмотрел несколько ссылок переполнения стека (см. ниже), и я не понимаю логику этого предупреждения и почему это считается плохой практикой кодирования. Я надеюсь, что другие могут помочь мне понять

я включу некоторый пример кода:

type    
  TMachine = class(TPersistent)
  private
  public
    Horsepower : integer;
    procedure Assign(Source : TMachine);
  end;

...

procedure TMachine.Assign(Source : TMachine);
begin
  inherited Assign(Source);
  Self.Horsepower := Source.HorsePower;
end;

это вызывает предупреждение компилятора.

[dcc32 Warning] Unit1.pas(21): W1010 Method 'Assign' hides virtual method of base type 'TPersistent'

я игнорировал это предупреждение, потому что он не сделал любой смысл для меня. Но у меня беда в другом (см. мой пост здесь: почему Delphi вызывает неправильный конструктор во время создания динамического объекта?) поэтому я решил попытаться понять это лучше.

Я знаю, что если я использую зарезервированное слово reintroduce, ошибка исчезнет,но я неоднократно видел, что это плохая идея. Как писал здесь Уоррен П (Delphi: метод "Create" скрывает виртуальный метод базы-но это правильно там), "ИМХО, Если вам нужно повторно ввести, ваш код пахнет ужасно".

Я думаю, что понимаю, что подразумевается под "скрытием". Как сказал Здесь Дэвид Хеффернан (что вызывает предупреждение" метод W1010 "%s "скрывает виртуальный метод базового типа "%s"?):

под скрытием подразумевается, что из производного класса у вас больше нет доступа к виртуальному методу, объявленному в базовом классе. Вы не можете ссылаться на него, так как он имеет то же имя, что и метод, объявленный в производном классе. И этот последний метод является тем, который виден из производного класса.

но я несколько смущен, потому что кажется, что метод предка на самом деле не скрыт, потому что производный класс всегда может просто использовать inherited ключевое слово для вызова метода в базовом классе. Значит, "скрытый" на самом деле означает "несколько скрытый"?

Я думаю, что я также понимаю, что использование зарезервированного слова override помешает предупреждение компилятора, но подпись процедуры должна быть одинаковой (т. е. никаких новых добавленных параметров). Что я не могу использовать это здесь.

чего я не понимаю, так это почему скрываться-это то, о чем нужно предупреждать. В моем примере кода выше я бы не хотел, чтобы пользователи TMachine.Assign() чтобы вместо этого как-то использовать TPersistent.Assign(). В моем расширенном классе у меня расширенные потребности, и поэтому я хочу, чтобы они использовали новую и улучшенную функцию. Поэтому кажется, что скрывать старый код-это именно то, что я хочу. Мой понимание virtual метод является одним, где правильный метод вызывается на основе фактического типа объекта во время выполнения. Не думаю, что это имеет какое-то отношение к делу.

дополнительный код, который будет добавлен в пример кода выше

  TAutomobile = class(TMachine)
  public
    NumOfDoors : integer;
    constructor Create(NumOfDoors, AHorsepower : integer);
  end;

...

constructor TAutomobile.Create(ANumOfDoors, AHorsepower : integer);
begin
  Inherited Create(AHorsepower);
  NumOfDoors := ANumOfDoors;
end;

это добавляет новое предупреждение компилятора:[dcc32 Warning] Unit1.pas(27): W1010 Method 'Create' hides virtual method of base type 'TMachine'

я особенно не понимаю проблем, которые возникают при использовании новых конструкторов с дополнительными параметрами. В этой должности (SerialForms.pas (17): метод W1010 "Create" скрывает виртуальный метод базового типа "TComponent"), мудрость, по-видимому, заключается в том, что должен быть введен конструктор с другим именем, например CreateWithSize. Казалось бы, это позволяет пользователям выбирать и выбирать, какой конструктор они хотят использовать.

и если они выбирают старый конструктор, расширенному классу может не хватать некоторой необходимой информации для создания. Но если вместо этого я "скрываю" предыдущий конструктор, это как-то плохо программируешь. Марьян Венема писал о reintroduce в этой же ссылке: Reintroduce ломает полиморфизм. Это означает, что вы больше не можете использовать мета-классы (TxxxClass = class Tyyy) для создания экземпляра вашего потомка TComponent, поскольку его Create не будет вызван. я вообще ничего не понимаю.

возможно, мне нужно лучше понять полиморфизм. Тони Старк написал в этой ссылке (что такое полиморфизм, для чего он и как его используют?) этот полиморфизм:"концепция объектно-ориентированного программирования.Способность различных объектов по-своему реагировать на идентичные сообщения называется полиморфизмом." Итак, я представляю другой интерфейс, т. е. больше не идентичное сообщение, и, таким образом, это нарушает полиморфизм?

что я упустил? Подводя итог, не является ли скрытие базового кода хорошей вещью в моих примерах?

2 ответов


опасность здесь в том, что вы можете позвонить Assign по ссылке на базовый класс. Потому что вы не используете override тогда ваш метод производного класса не вызывается. Таким образом, вы разрушили полиморфизм.

по принципу наименьшего удивления вы должны использовать override здесь или дайте методу производного класса другое имя. Последний вариант прост. Первый выглядит так:

type    
  TMachine = class(TPersistent)
  public
    Horsepower : integer;
    procedure Assign(Source : TPersistent); override;
  end;

...

procedure TMachine.Assign(Source : TPersistent);
begin
  if Source is TMachine then begin
    Horsepower := TMachine(Source).Horsepower;
  end else begin
    inherited Assign(Source);
  end;
end;

это позволяет вашему классу сотрудничать с полиморфным дизайном TPersistent. Без использования override это было бы невозможно.

ваш следующий пример с виртуальными конструкторами аналогичен. Весь смысл создания виртуального конструктора заключается в том, что вы можете создавать экземпляры, не зная их типа до выполнения. Каноническим примером является streaming framework, платформа, которая обрабатывает .ДФМ./FMX файлы и создает объекты и устанавливает их свойства.

что streaming framework полагается на виртуальный конструктор TComponent:

constructor Create(AOwner: TComponent); virtual;

если вы хотите, чтобы компонент работал с Streaming framework, необходимо переопределить этот конструктор. Если вы спрячете его, то streaming framework не сможет найти ваш конструктор.

рассмотрим, как streaming framework создает экземпляры компонентов. Он не знает обо всех классах компонентов, с которыми ему нужно работать. Он не может, например, рассматривать сторонний код, код, который вы пишете. Delphi RTL не может знать о типах, определенных там. Этот streaming framework создает такие компоненты:

type
  TComponentClass = class of TComponent;

var
  ClassName: string;
  ClassType: TComponentClass;
  NewComponent: TComponent;

....
ClassName := ...; // read class name from .dfm/.fmx file
ClassType := GetClass(ClassName); // a reference to the class to be instantiated
NewComponent := ClassType.Create(...); // instantiate the component

на ClassType переменная содержит мета-класса. Это позволяет нам представлять тип, который не известен до выполнения. Нам нужно позвонить в Create будет отправлен полиморфно, так что код в конструкторе компонента будет выполнен. Если вы не используете override при объявлении этого конструктора этого не будет.

действительно, все это сводится к полиморфизму. Если ваше понимание полиморфизма не твердый, как вы предполагаете, тогда вы будете бороться, чтобы оценить все это. Я думаю, что ваш следующий шаг-лучше понять, что такое полиморфизм.


существуют различные преимущества использования наследования. В ваших примерах вы делаете это, чтобы избежать кодирования одних и тех же вещей снова и снова. Так что если TMachine и Horsepower поле уже и некоторые методы, и теперь вам нужно более продвинутый TAutomobile С NumOfDoors, ты TMachine потомок.

если вы теперь всегда относитесь к ним по-разному, я.e в некотором коде вы используете именно TMachine (machine := TMachine.Create(...), machine.Assign(AnotherMachine) etc. ) и в другом коде вы используете TAutomobile и они никогда не смешиваются тогда вы все правильно, вы можете игнорировать эти предупреждения или "отключить" их с помощью reintroduce.

но обычно есть еще один аспект наследования: сохранение единого интерфейса или, как его иногда называют: "контракт". Отделение интерфейса от реализации.

например, форма способна освободить все объекты, которые ей принадлежат, независимо от того, что это за объекты, это из-за Destroy метод, который получает перекрытая. Форма не заботится о вашей реализации, но она знает: освободить объект он просто должен вызвать Destroy, что легко. Если вы не переопределите Destroy, это очень плохо: ни в коем случае TForm будет называть вас как TMachine.Destroy. Он будет называть вас как TObject.Destroy, но это не приведет к вашей TMachine.Destroy, таким образом, вы получаете утечку памяти. В большинстве случаев, когда какой-либо метод не был переопределен, это просто потому, что программист забыл это сделать, поэтому предупреждение: это очень полезно. Если программист не забыл об этом, но это было намеренно, ключевое слово. Таким образом программист говорит: "Да, я знай, что я делаю, это нарочно, не мешай мне!"

TPersistent.Assign еще одна процедура, которая часто вызывается из базового класса, а не производного (то есть: мы не хотим обращать внимание на выполнение, мы просто хотим скопировать объект, что бы это ни было). Например, TMemo и Lines: TStrings, а TStrings является абстрактным классом, в то время как фактическая реализация TStringList. Итак, когда вы пишете Memo1.Lines.Assign(Memo2.Lines), the метод. Он может реализовать это назначение через другое методы: сначала очистите себя, а затем добавьте строку за строкой. Некоторые TStrings потомок может захотеть ускорить процесс с помощью некоторой блочной копии данных. Конечно, он должен использовать метод Assign(Source: TPersistent) и переопределять его, иначе он никогда не вызывается (вместо этого вызывается унаследованный).

классическая реализация Assign выглядит так:

procedure TMachine.Assign(Source : TPersistent);
begin
  if Source is TMachine then
    Horsepower := TMachine(Source).Horsepower
  else inherited Assign(Source);
end;

это тот случай, когда inherited не следует называть первым делом. Здесь это "последнее средство": это называется последним, если больше ничего не помогало. Это делает последнюю попытку: если ваш класс не знает, как назначить, возможно, это Source умеет AssignTo ваш класс?

например, TBitmap был написан давным-давно. После этого TPngImage был разработан для работы с, ну, PNG. Вы хотите поместить PNG в растровое изображение и написать:Bitmap.Assign(PngImage). Ни TBitmap может знать, как бороться с PNG: его тогда не существовало! Но!--30--> писатель знал, что может случиться и реализован AssignTo метод, который способен преобразовать его в Bitmap. Так что TBitmap как зовет последняя капля TPersistent.Assign способ и что в свою очередь вызывает TPngImage.AssignTo и это работает как шарм.

эта сторона наследования, необходимая в вашей программе, зависит от вас. Если снова много дублирующего кода (тот, который имеет дело с машинами, а другой-с автомобилями) или есть много условий, то что-то не так, и какой-то хороший полиморфизм может помочь.