Какова была мотивация наличия ключевого слова reintroduce
в Delphi?
Если у вас есть дочерний класс, который содержит функцию с тем же именем, что и виртуальная функция в родительском классе, и она не объявлена с помощью модификатора переопределения, то это ошибка компиляции. Добавление повторного введения модификатора в таких ситуациях исправляет ошибку, но я никогда не понимал аргументацию ошибки компиляции.
Если вы объявляете метод в классе потомков, который имеет то же имя, что и метод в классе предка, тогда вы скрываете этот метод предка - это означает, что если у вас есть экземпляр этого класса потомков (на который ссылается этот класс) то вы не получите поведения предка. Когда метод предка является виртуальным или динамическим, компилятор выдаст вам предупреждение.
Теперь у вас есть один из двух вариантов для подавления этого предупреждающего сообщения:
Таким образом, разница между переопределением и reintroduce заключается в полиморфизме. Если повторно ввести, если вы передаете объект-потомка в качестве родительского типа, тогда вызовите этот метод, вы получите метод предка, но если вы получите к нему тип потомка, вы получите поведение потомка. При переопределении вы всегда получаете потомка. Если метод предка не был ни виртуальным, ни динамическим, то повторно ввести не применяется, потому что это поведение неявно. (На самом деле вы могли бы использовать помощник класса, но мы не пойдем туда сейчас.)
Несмотря на то, что сказал Малач, вы можете по-прежнему вызывать унаследованный в повторно введенный метод, даже если родительский элемент не был виртуальным и динамическим.
По сути, это просто как переопределить, но оно работает с не-сильными динамическими и не виртуальными методами, и оно не заменяет если экземпляр объекта доступен через выражение типа предка.
Дальнейшее объяснение:
Reintroduce - это способ передачи намерения компилятору, что вы не сделали ошибку. Мы переопределяем метод в предке с ключевым словом переопределить, но для этого требуется, чтобы метод предка был виртуальным или динамическим, и что вы хотите, чтобы поведение, которое нужно изменить при доступе к объекту в качестве класса предка. Теперь введите повторно. Он позволяет сообщить компилятору, что вы случайно не создали метод с тем же именем, что и метод виртуального или динамического предка (что было бы неприятно, если компилятор не предупредил вас об этом).
Здесь много ответов о том, почему компилятор, который позволяет вам скрывать функцию-член молча, является плохой идеей. Но ни один современный компилятор не скрывает функции-члены. Даже в С++, где это разрешено, всегда есть предупреждение об этом, и этого должно быть достаточно.
Итак, зачем требовать "повторного ввода"? Основная причина в том, что это ошибка, которая может появиться случайно, когда вы больше не смотрите на предупреждения компилятора. Например, предположим, что вы наследуете от TComponent, а дизайнеры Delphi добавляют новую виртуальную функцию в TComponent. Плохая новость - это ваш производный компонент, который вы написали пять лет назад и распространяемый другим, уже имеет функцию с этим именем.
Если компилятор просто принял эту ситуацию, какой-то конечный пользователь может перекомпилировать ваш компонент, проигнорируйте это предупреждение. Странные вещи произойдут, и вас обвинят. Это требует, чтобы они явно признавали, что функция не является той же самой функцией.
RTL использует повторно для скрытия унаследованных конструкторов. Например, у TComponent есть конструктор, который принимает один аргумент. Но, TObject имеет конструктор без параметров. RTL хотел бы, чтобы вы использовали только конструктор с одним аргументом TComponent, а не конструктор без параметров, унаследованный от TObject при создании экземпляра нового TComponent. Поэтому он использует повторно для скрытия наследуемого конструктора. Таким образом, повторное введение немного напоминает объявление конструктора без параметров без ограничений в С#.
reintroduce
в TComponent.Create
.
tl; dr: Попытка переопределить не виртуальный метод не имеет смысла. Добавьте ключевое слово reintroduce
, чтобы подтвердить, что вы совершаете ошибку.
Целью повторного введения модификатора является предотвращение общей логической ошибки.
Я предполагаю, что общеизвестно, как повторно вводить ключевое слово, фиксирует предупреждение и объясняет, почему генерируется предупреждение и почему ключевое слово включено в язык. Рассмотрим приведенный ниже код 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 был спроектирован так, чтобы программист мягко убеждал, порождая предупреждение, явно указывать свои намерения в таких ситуациях.
Прежде всего, "повторно ввести" разрушает цепочку наследования и не следует использовать не, и я имею в виду никогда никогда. За все время работы с Delphi (около 10 лет) я наткнулся на ряд мест, которые используют это ключевое слово, и это всегда было ошибкой в дизайне.
С учетом этого простейший способ работы:
Как я сказал это чистое зло и его следует избегать любой ценой (ну, по крайней мере, мое мнение). Это как использование goto - просто страшный стиль: D
Reintroduce
не разрывает цепочку наследования. Подавляет предупреждение о разрыве цепочки наследования.
Reintroduce сообщает компилятору, что вы хотите вызвать код, определенный в этом методе, как точку входа для этого класса и его потомков, независимо от других методов с тем же именем в цепочке предков.
Создание TDescendant.MyMethod
создаст потенциальную путаницу для TDescendants при добавлении другого метода с тем же именем, о котором компилятор предупреждает вас.
Повторно вводите disambiguates, что и сообщает компилятору, что вы знаете, какой из них использовать. ADescendant.MyMethod
вызывает TDescendant one, (ADescendant as TAncestor).MyMethod
вызывает имя TAncestor. Всегда! Нет путаницы.... Компилятор счастлив!
Это верно, хотите ли вы, чтобы метод потомка был виртуальным или нет: в обоих случаях вы хотите нарушить естественную связь виртуальной сети. И это не мешает вам вызвать унаследованный код из нового метода.
Во-первых, как было сказано выше, вы никогда не должны преднамеренно повторно вводить виртуальный метод. Единственное разумное использование повторного ввода - это когда автор предка (а не вы) добавил метод, который вступает в конфликт с вашим потомком и переименовывает ваш метод потомков, не является вариантом. Во-вторых, вы можете легко вызвать исходную версию виртуального метода даже в классах, где вы повторно ввели его с различными параметрами:
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;
так что должно быть целью повторного введения виртуального метода, кроме того, чтобы сделать что-то труднее читать?
Это было введено для языка из-за версий Framework (включая VCL).
Если у вас есть существующая база кода и обновление Framework (например, потому что вы купили новую версию Delphi), ввели виртуальный метод с тем же именем, что и метод в предке вашей базы кода, тогда reintroduce
позволит вам избавиться от предупреждения W1010.
Это единственное место, где вы должны использовать reintroduce
.
Когда класс предков также имеет метод с тем же именем и не обязательно объявляется виртуальным, вы увидите предупреждение компилятора (так как вы скрыли бы этот метод).
Другими словами: вы сообщаете компилятору, что знаете, что вы скрываете функцию предка и заменяете ее этой новой функцией и делаете это намеренно.
И зачем вы это делаете? Если метод является виртуальным в родительском классе, единственной причиной является предотвращение полиморфизма. Другие тогда просто переопределяют и не наследуют. Но если родительский метод не объявлен виртуальным (и вы не можете его изменить, потому что вы не владеете кодом, например), вы можете наследовать этот класс и позволить людям наследовать от вашего класса, не видя предупреждения компилятора.
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;