Повторное введение функций в Delphi - PullRequest
32 голосов
/ 10 сентября 2008

Что послужило причиной наличия ключевого слова reintroduce в Delphi?

Если у вас есть дочерний класс, который содержит функцию с тем же именем, что и у виртуальной функции в родительском классе, и он не объявлен с модификатором override, то это ошибка компиляции. Добавление модификатора reintroduce в таких ситуациях исправляет ошибку, но я никогда не понимал причины ошибки компиляции.

Ответы [ 11 ]

64 голосов
/ 12 сентября 2008

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

Теперь у вас есть один из двух вариантов подавления этого предупреждающего сообщения:

  1. Добавление ключевого слова reintroduce просто сообщает компилятору, что вы знаете, что вы скрываете этот метод, и подавляет предупреждение. Вы все еще можете использовать унаследованное ключевое слово в своей реализации этого потомкового метода для вызова метода предка.
  2. Если метод предка был виртуальный или динамический , тогда вы можете использовать override . Он имеет дополнительное поведение, что если доступ к этому объекту-потомку осуществляется через выражение типа предка, то вызов этого метода все равно будет к методу-потомку (который затем может опционально вызвать предка через унаследовано ) .

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

Несмотря на то, что сказал Малах, вы все еще можете вызывать унаследованное в повторно введенном методе, даже если родитель не был ни виртуальным , ни динамическим .

По сути, повторное введение аналогично переопределению , но оно работает с не динамическими и не виртуальными методами, и не заменяет поведение, если доступ к экземпляру объекта осуществляется через выражение типа предка.

Дополнительные пояснения:

Повторное введение - это способ сообщить намерению компилятору, что вы не допустили ошибку. Мы переопределяем метод в предке с помощью ключевого слова override , но для этого необходимо, чтобы метод предка был virtual или dynamic , и чтобы вы изменили поведение когда объект доступен как класс предка. Теперь введите , введите . Это позволяет вам сообщить компилятору, что вы случайно не создали метод с тем же именем, что и метод виртуального или динамического предка (что будет раздражать, если компилятор не предупредит вас об этом).

7 голосов
/ 17 сентября 2008

Здесь много ответов о том, почему компилятор, который позволяет скрыть функцию-член без вывода сообщений , является плохой идеей. Но ни один современный компилятор молча не скрывает функции-члены. Даже в C ++, где это разрешено, всегда есть предупреждение об этом, и этого должно быть достаточно.

Так зачем требовать "повторного введения"? Основная причина в том, что это ошибка, которая может появиться случайно, когда вы больше не смотрите на предупреждения компилятора. Например, предположим, что вы наследуете от TComponent, а разработчики Delphi добавляют новую виртуальную функцию в TComponent. Плохая новость заключается в том, что ваш производный компонент, который вы написали пять лет назад и раздали другим, уже имеет функцию с таким именем.

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

4 голосов
/ 15 сентября 2008

RTL использует reintroduce, чтобы скрыть унаследованные конструкторы. Например, TComponent имеет конструктор, который принимает один аргумент. Но у TObject есть конструктор без параметров. RTL хотел бы, чтобы вы использовали только конструктор TComponent с одним аргументом, а не конструктор без параметров, унаследованный от TObject, при создании нового TComponent. Поэтому он использует reintroduce, чтобы скрыть унаследованный конструктор. Таким образом, повторное введение немного похоже на объявление конструктора без параметров как частного в C #.

3 голосов
/ 20 января 2011

tl; dr: Попытка переопределить не виртуальный метод не имеет смысла. Добавьте ключевое слово reintroduce, чтобы подтвердить, что вы делаете ошибку.

3 голосов
/ 30 декабря 2008

Прежде всего, «реинтродукция» разрывает цепочку наследования и должна не использоваться, а я имею в виду никогда . За все время работы с Delphi (около 10 лет) я наткнулся на ряд мест, в которых используется это ключевое слово, и это всегда было ошибкой в ​​дизайне.

Имея это в виду, вот самый простой способ, которым это работает:

  1. У вас есть как виртуальный метод в базовом классе
  2. Теперь вы хотите иметь метод с точно таким же именем, но, возможно, с другой подписью. Таким образом, вы пишете свой метод в производном классе с тем же именем, и он не будет компилироваться, потому что контракт не выполнен.
  3. Вы поместили туда ключевое слово reintroduce , и ваш базовый класс не знает о вашей новой реализации, и вы можете использовать его only при доступе к вашему объекту из напрямую указанного типа экземпляра , Это означает, что игрушка не может просто присвоить объект переменной базового типа и вызвать этот метод, потому что его нет с нарушенным контрактом.

Как я и сказал, это чистое зло, и его нужно избегать любой ценой (ну, по крайней мере, это мое мнение). Это как использовать goto - просто ужасный стиль: D

3 голосов
/ 27 сентября 2008

Цель модификатора повторного ввода - предотвратить распространенную логическую ошибку.

Я предполагаю, что общеизвестно, как повторное введение ключевого слова исправляет предупреждение и объяснит, почему генерируется предупреждение и почему ключевое слово включено в язык. Рассмотрим приведенный ниже код delphi;

TParent = Class
Public
    Procedure Procedure1(I : Integer); Virtual;
    Procedure Procedure2(I : Integer);
    Procedure Procedure3(I : Integer); Virtual;
End;

TChild = Class(TParent)
Public
    Procedure Procedure1(I : Integer);
    Procedure Procedure2(I : Integer);
    Procedure Procedure3(I : Integer); Override;
    Procedure Setup(I : Integer);
End;

procedure TParent.Procedure1(I: Integer);
begin
    WriteLn('TParent.Procedure1');
end;

procedure TParent.Procedure2(I: Integer);
begin
    WriteLn('TParent.Procedure2');
end;

procedure TChild.Procedure1(I: Integer);
begin
    WriteLn('TChild.Procedure1');
end;

procedure TChild.Procedure2(I: Integer);
begin
    WriteLn('TChild.Procedure2');
end;

procedure TChild.Setup(I : Integer);
begin
    WriteLn('TChild.Setup');
end;

Procedure Test;
Var
    Child : TChild;
    Parent : TParent;
Begin
    Child := TChild.Create;
    Child.Procedure1(1); // outputs TChild.Procedure1
    Child.Procedure2(1); // outputs TChild.Procedure2

    Parent := Child;
    Parent.Procedure1(1); // outputs TParent.Procedure1
    Parent.Procedure2(1); // outputs TParent.Procedure2
End;

Учитывая приведенный выше код, обе процедуры в TParent скрыты. Сказать, что они скрыты, означает, что процедуры не могут быть вызваны через указатель TChild. Компиляция примера кода выдает одно предупреждение;

[Предупреждение DCC] Project9.dpr (19): Метод W1010 «Процедура1» скрывает виртуальный метод базового типа «TParent»

Почему только предупреждение для виртуальной функции, а не для другой? Оба скрыты.

Достоинство Delphi заключается в том, что разработчики библиотек могут выпускать новые версии, не опасаясь нарушить логику существующего клиентского кода. Это отличается от Java, где добавление новых функций к родительскому классу в библиотеке сопряжено с опасностью, поскольку классы неявно являются виртуальными. Допустим, что TParent свыше живет в сторонней библиотеке, а изготовитель библиотеки выпускает новую версию ниже.

// version 2.0
TParent = Class
Public
    Procedure Procedure1(I : Integer); Virtual;
    Procedure Procedure2(I : Integer);
    Procedure Procedure3(I : Integer); Virtual;
    Procedure Setup(I : Integer); Virtual;
End;

procedure TParent.Setup(I: Integer);
begin
    // important code
end;

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

Procedure TestClient;
Var
    Child : TChild;
Begin
    Child := TChild.Create;
    Child.Setup;
End;

Для клиента не имеет значения, скомпилирован ли код с версией 2 или 1 библиотеки, в обоих случаях TChild.Setup вызывается по назначению пользователя. И в библиотеке;

// library version 2.0
Procedure TestLibrary(Parent : TParent);
Begin
    Parent.Setup;
End;

Если TestLibrary вызывается с параметром TChild, все работает как задумано. Разработчик библиотеки не знает о TChild.Setup, и в Delphi это не причиняет им никакого вреда. Вызов выше правильно разрешает TParent.Setup.

Что произойдет в эквивалентной ситуации в Java? TestClient будет работать правильно, как задумано. TestLibrary не будет. В Java все функции предполагаются виртуальными. Parent.Setup преобразуется в TChild.Setup, но помните, что когда был написан TChild.Setup, они не знали о будущем TParent.Setup, поэтому они, конечно, никогда не будут вызывать наследуемый. Поэтому, если разработчик библиотеки намеревался вызвать TParent.Setup, его не будет, независимо от того, что они делают. И, конечно, это может быть катастрофическим.

Таким образом, объектная модель в Delphi требует явного объявления виртуальных функций в цепочке дочерних классов. Побочным эффектом этого является то, что легко забыть добавить модификатор override для дочерних методов. Наличие ключевого слова Reintroduce удобно для программиста. Delphi был спроектирован таким образом, чтобы при генерации предупреждения программиста аккуратно убедили явно заявить о своих намерениях в таких ситуациях.

2 голосов
/ 16 сентября 2008

Reintroduce сообщает компилятору, что вы хотите вызвать код, определенный в этом методе, как точку входа для этого класса и его потомков, независимо от других методов с тем же именем в цепочке предков.

Создание TDescendant.MyMethod может создать путаницу для TDescendants при добавлении другого метода с тем же именем, о котором вас предупреждает компилятор.
Повторное введение устраняет неоднозначность и сообщает компилятору, что вы знаете, какой использовать.
ADescendant.MyMethod вызывает TDescendant, (ADescendant as TAncestor).MyMethod вызывает TAncestor. Всегда! Никакой путаницы ... Компилятор доволен!

Это верно, хотите ли вы, чтобы метод-потомок был виртуальным или нет: в обоих случаях вы хотите разорвать естественную связь виртуальной цепочки. И это не мешает вам вызывать унаследованный код из нового метода.

  1. TDescendant.MyMethod является виртуальным: ... но вы не можете или не хотите использовать связь.
    • Вы не можете, потому что подпись метода отличается. У вас нет другого выбора, так как в этом случае переопределение невозможно, если тип возвращаемого значения или параметры не совпадают.
    • Вы хотите перезапустить дерево наследования из этого класса.
  2. TDescendant.MyMethod не является виртуальным: вы превращаете MyMethod в статический на уровне TDescendant и предотвращаете дальнейшее переопределение. Все классы, наследуемые от TDescendant, будут использовать реализацию TDescendant.
2 голосов
/ 10 сентября 2008

Когда у класса-предка также есть метод с тем же именем, и он не обязательно объявлен виртуальным, вы увидите предупреждение компилятора (как если бы вы скрывали этот метод).

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

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

1 голос
/ 13 ноября 2017

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

type 
  tMyFooClass = class of tMyFoo;

  tMyFoo = class
    constructor Create; virtual;
  end;

  tMyFooDescendant = class(tMyFoo)
    constructor Create(a: Integer); reintroduce;
  end;


procedure .......
var
  tmp: tMyFooClass;
begin
  // Create tMyFooDescendant instance one way
  tmp := tMyFooDescendant;
  with tmp.Create do  // please note no a: integer argument needed here
  try
    { do something }
  finally
    free;
  end;

  // Create tMyFooDescendant instance the other way
  with tMyFooDescendant.Create(20) do  // a: integer argument IS needed here
  try
    { do something }
  finally
    free;
  end;

Так, какова должна быть цель повторного введения виртуального метода, кроме как затруднить чтение?

1 голос
/ 20 января 2011

Это было введено в язык из-за версий Framework (включая VCL).

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

Это единственное место, где вы должны использовать reintroduce.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...