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





Когда класс-предок также имеет метод с тем же именем, и он не обязательно объявлен виртуальным, вы увидите предупреждение компилятора (как если бы вы скрыли этот метод).
Другими словами: вы сообщаете компилятору, что знаете, что скрываете функцию-предок и заменяете ее этой новой функцией, и делаете это намеренно.
И зачем вам это делать? Если метод является виртуальным в родительском классе, единственная причина - предотвратить полиморфизм. В остальном это просто переопределяет и не вызывает унаследованный. Но если родительский метод не объявлен виртуальным (и вы не можете это изменить, потому что, например, вы не владеете кодом), вы можете наследовать от этого класса и позволить людям наследовать от вашего класса, не видя предупреждения компилятора.
Не могли бы вы пояснить, что подразумевается под «как бы вы скрыли этот метод и не смогли бы вызвать унаследованный»?
Держу пари, есть веская причина для повторного введения вместо отмены. Ваш родительский класс может вызывать виртуальный метод, но вы не хотите, чтобы эти вызовы попадали в ваш повторно введенный метод.
@mliesen Если вы не хотите, чтобы вызовы базового класса попадали в ваш «повторно введенный» метод - просто используйте другое имя метода. Вы не «решите» проблему банальный, разорвав цепочку наследования.
Если вы объявляете метод в классе-потомке, который имеет то же имя, что и метод в классе-предке, вы скрываете этот метод-предок - это означает, что если у вас есть экземпляр этого класса-потомка (который упоминается как этот класс), вы будете не получить поведение предка. Когда метод предка является виртуальным или динамическим, компилятор выдает предупреждение.
Теперь у вас есть один из двух вариантов подавления этого предупреждающего сообщения:
Итак, разница между отвергать и повторно ввести заключается в полиморфизме. С повторно ввести, если вы приведете объект-потомок как родительский тип, а затем вызовете этот метод, вы получите метод-предок, но если вы получите доступ к нему, к типу-потомку, вы получите поведение потомка. С отвергать вы всегда получаете потомка. Если метод-предок не был ни виртуальный, ни динамичный, то повторно ввести не применяется, потому что это поведение неявно. (На самом деле вы можете использовать помощник класса, но сейчас мы не будем туда заходить.)
Несмотря на то, что сказал Малах, вы все еще может вызвать унаследованный в повторно введенном методе, даже если родитель не был ни виртуальный, ни динамичный.
По сути, reintroduce похож на отвергать, но он работает с методами, отличными от динамичный и не виртуальный, и не заменяет - это поведение, если к экземпляру объекта осуществляется доступ через выражение типа предка.
Дальнейшее объяснение:
Повторно ввести - это способ сообщить компилятору о том, что вы не сделали ошибки. Мы переопределяем метод в предке с помощью ключевого слова отвергать, но для этого требуется, чтобы метод предка был виртуальный или динамичный, и что вы хотите, чтобы поведение изменялось при доступе к объекту как к классу-предку. Теперь введите повторно ввести. Он позволяет сообщить компилятору, что вы не случайно создали метод с тем же именем, что и виртуальный или динамический метод-предок (что было бы неприятно, если бы компилятор не предупредил вас).
Вы сказали мне, что делает ключевое слово reintroduce. По сути, вы сказали, что это означает, что функция не виртуальная. Да, но при этом к функции не добавляются модификаторы virtual / overide / dynamic. Но почему Андерс Хейлсберг решил, что необходимо добавить в язык ключевое слово reintroduce?
Приятно иметь возможность компиляции уводить вас от потенциальных ошибок. Но с таким же успехом об этом можно было сообщить как о «предупреждении», а не об ошибке.
RTL использует повторное введение, чтобы скрыть унаследованные конструкторы. Например, у TComponent есть конструктор, который принимает один аргумент. Но у TObject есть конструктор без параметров. RTL хотел бы, чтобы вы использовали только конструктор с одним аргументом TComponent, а не конструктор без параметров, унаследованный от TObject, при создании экземпляра нового TComponent. Таким образом, он использует повторное введение, чтобы скрыть унаследованный конструктор. Таким образом, повторное введение немного похоже на объявление конструктора без параметров закрытым в C#.
Я не вижу reintroduce в TComponent.Create.
@RobKennedy Ага, не нужно здесь повторно вводить, хотя пример неверен, но основной. Интересно, есть ли какая-то магия компилятора, которая подавляет это предупреждение при наследовании от TObject.
Reintroduce сообщает компилятору, что вы хотите вызвать код, определенный в этом методе, как точку входа для этого класса и его потомков, независимо от других методов с тем же именем в цепочке предков.
Создание TDescendant.MyMethod может создать потенциальную путаницу для потомков TD при добавлении другого метода с тем же именем, о чем вас предупреждает компилятор.
Reintroduce устраняет неоднозначность и сообщает компилятору, что вы знаете, какой из них использовать.
ADescendant.MyMethod вызывает TDescendant, (ADescendant as TAncestor).MyMethod вызывает TAncestor. Всегда! Без путаницы…. Компилятор доволен!
Это верно независимо от того, хотите ли вы, чтобы метод-потомок был виртуальным: в обоих случаях вы хотите разорвать естественную связь виртуальной цепочки. И это не мешает вам вызывать унаследованный код из нового метода.
Здесь есть много ответов о том, почему компилятор, позволяющий скрыть функцию-член тихо, - плохая идея. Но ни один современный компилятор тихо не скрывает функции-члены. Даже в C++, где это разрешено, всегда есть предупреждение об этом, и этого должно быть достаточно.
Так зачем требовать «повторного введения»? Основная причина в том, что это ошибка, которая может появиться случайно, когда вы больше не смотрите предупреждения компилятора. Например, предположим, что вы наследуете от TComponent, а дизайнеры Delphi добавляют новую виртуальную функцию в TComponent. Плохая новость заключается в том, что ваш производный компонент, который вы написали пять лет назад и распространил среди других, уже имеет функцию с таким именем.
Если компилятор просто принял эту ситуацию, какой-то конечный пользователь может перекомпилировать ваш компонент, игнорируя предупреждение. Происходят странные вещи, и обвиняют ты. Это требует от них явного признания того, что функция не является той же самой функцией.
Назначение модификатора reintroduce - предотвратить обычную логическую ошибку.
Я предполагаю, что всем известно, как ключевое слово 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 'Procedure1' скрывает виртуальный метод базового типа '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 лет), я наткнулся на ряд мест, где действительно используется это ключевое слово, и это всегда было ошибкой в дизайне.
Имея это в виду, вот простейший способ работы:
Как я уже сказал, это чистое зло, и его следует избегать любой ценой (ну, по крайней мере, это мое мнение). Это как использовать идти к - просто ужасный стиль: D
Reintroduce не разрывает цепочку наследования. Он подавляет предупреждение о, нарушающее цепочку наследования.
Я думаю, что исключением являются конструкторы, они позволяют вам (повторно) вводить конструкторы с параметрами - иначе вы не сможете сделать это без предупреждения (кроме случаев наследования от дерева классов, у которого нет конструкторов).
tl; dr: Нет смысла пытаться переопределить невиртуальный метод. Добавьте ключевое слово reintroduce, чтобы подтвердить, что вы делаете ошибку.
Попытка переопределить невиртуальный метод всегда является ошибкой. Никакие дополнительные директивы не заставят компилятор передумать.
Это было введено в язык из-за версий Framework (включая VCL).
Если у вас есть существующая база кода и обновление Framework (например, потому что вы купили более новую версию Delphi) представило виртуальный метод с тем же именем, что и метод в предке вашей базы кода, то reintroduce позволит вам избавьтесь от Предупреждение W1010.
Это единственное место, где следует использовать reintroduce.
Или вместо использования повторно ввести, чтобы зарыть голову в песок: просто переименуйте свой метод, чтобы больше не нарушать цепочку наследования. (Без инструмента рефакторинга вы можете использовать поиск и замену в крайнем случае.)
@CraigYoung, который очень хорошо работает с новой или кодовой базой и может работать с меньшими существующими кодовыми базами, но часто создает огромные проблемы с более крупными существующими кодовыми базами.
повторно ввести позволяет вам объявить метод с тем же именем, что и у предка, но с другими параметрами. Это не имеет ничего общего с ошибками или ошибками !!!
Например, я часто использую его для конструкторов ...
constructor Create (AOwner : TComponent; AParent : TComponent); reintroduce;
Это позволяет мне создавать внутренние классы более чистым образом для сложных элементов управления, таких как панели инструментов или календари. Обычно у меня больше параметров. Иногда для создания класса без передачи некоторых параметров используется почти невозможно или очень запутанно.
Для визуальных элементов управления Application.Processmessages можно вызвать после Create, что может быть слишком поздно для использования этих параметров.
constructor TClassname.Create (AOwner : TComponent; AParent : TComponent);
begin
inherited Create (AOwner);
Parent := AParent;
..
end;
Удачи с этим. Что вы делаете, так это ставите себя в ситуацию, когда потоковая передача компонентов VCL больше не может вести себя так, как она была спроектирована; особенно с подклассами ваших компонентов.
Во-первых, как было сказано выше, никогда не следует намеренно повторно вводить виртуальный метод. Единственное разумное использование повторного введения - это когда автор предка (а не вы) добавил метод, который вступает в конфликт с вашим потомком, и переименование вашего метода потомка не является вариантом. Во-вторых, вы можете легко вызвать исходную версию виртуального метода даже в классах, где вы повторно ввели его с другими параметрами:
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;
так в чем же должна быть цель повторного введения виртуального метода, кроме как усложнить чтение?
Самый высокий ответ на этот вопрос действительно правильный, не могли бы вы отметить его таким образом?