Повторное введение функций в Delphi

30

Какова была мотивация наличия ключевого слова reintroduce в Delphi?

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

  • 6
    Самый лучший ответ на этот вопрос действительно правильный, не могли бы вы отметить его таким образом?
Теги:
polymorphism
oop

11 ответов

58

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

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

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

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

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

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

Дальнейшее объяснение:

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

  • 0
    Вы сказали мне, что делает ключевое слово reintroduce. В основном вы сказали, что это означает, что функция не является виртуальной. Да, но не добавляет в функцию виртуальный / оверидный / динамический модификатор. Но почему Андерс Хейлсберг решил, что необходимо добавить в язык ключевое слово reintroduce?
  • 1
    Приятно, что компилятор отвлекает вас от потенциальных ошибок. Но с тем же успехом можно было бы сообщить о «предупреждении» об ошибке.
7

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

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

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

4

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

  • 0
    Я не вижу reintroduce в TComponent.Create .
  • 0
    @RobKennedy Да, не вводите здесь снова, хотя пример неверен, основной принцип. Интересно, есть ли какая-то магия компилятора, которая подавляет это предупреждение при наследовании от TObject.
3

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

  • 2
    Попытка переопределить не виртуальный метод всегда является ошибкой. Никакие дополнительные директивы не заставят компилятор передумать об этом.
3

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

Я предполагаю, что общеизвестно, как повторно вводить ключевое слово, фиксирует предупреждение и объясняет, почему генерируется предупреждение и почему ключевое слово включено в язык. Рассмотрим приведенный ниже код 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 требует явного объявления виртуальных функций по цепочке дочерних классов. Побочным эффектом этого является то, что легко забыть добавить модификатор переопределения в дочерние методы. Существование ключевого слова Reintroduce - это удобство для программиста. Delphi был спроектирован так, чтобы программист мягко убеждал, порождая предупреждение, явно указывать свои намерения в таких ситуациях.

  • 0
    Вы кратко ответили на свой собственный вопрос великолепно, но ... Почему снова вводить ключевое слово, а не параметр компилятора, когда его полезность настолько мала?
  • 1
    Потому что , когда это полезно , это очень полезно. Предотвращает некоторые вещи от несчастного случая.
2

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

С учетом этого простейший способ работы:

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

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

  • 0
    Reintroduce не разрывает цепочку наследования. Подавляет предупреждение о разрыве цепочки наследования.
  • 1
    Я думаю, что исключением здесь являются конструкторы, они позволяют (повторно) вводить конструкторы с параметрами - в противном случае вы не сможете сделать это без предупреждения (за исключением случаев наследования из дерева классов, в котором нет конструкторов).
2

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

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

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

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

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

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

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

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

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

  • 0
    Или вместо того, чтобы использовать reintroduce, чтобы похоронить голову в песке: просто переименуйте свой метод, чтобы больше не разрывать цепочку наследования. (Без инструмента рефакторинга вы можете использовать поиск и замену в крайнем случае.)
  • 0
    @CraigYoung, который очень хорошо работает в новой или кодовой базе и может работать в меньших существующих кодовых базах, но часто создает огромные головные боли в больших существующих кодовых базах.
1

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

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

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

  • 0
    Не могли бы вы уточнить, что подразумевается под «как вы бы скрывали этот метод и не могли вызывать наследуемый»?
  • 0
    Держу пари, что есть веская причина для повторного введения вместо переопределения. Ваш родительский класс может выполнять вызовы виртуального метода, но вы не хотите, чтобы эти вызовы доходили до вашего повторно введенного метода.
Показать ещё 1 комментарий
0

reintroduce позволяет объявить метод с тем же именем, что и предок, но с разными параметрами. Это не имеет никакого отношения к ошибкам или ошибкам!

Например, я часто использую его для конструкторов...

constructor Create (AOwner : TComponent; AParent : TComponent); reintroduce;

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

Для визуальных элементов управления Application.Processmessages можно вызвать после Create, что может быть слишком поздно для использования этих параметров.

constructor TClassname.Create (AOwner : TComponent; AParent : TComponent);
begin
  inherited Create (AOwner);
  Parent      := AParent;
  ..
end;
  • 2
    Удачи с этим. Вы попадаете в ситуацию, когда потоковая передача компонентов VCL больше не может вести себя так, как она была задумана; особенно с подклассами ваших компонентов.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню