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

Что побудило использовать ключевое слово reintroduce в Delphi?

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

Самый высокий ответ на этот вопрос действительно правильный, не могли бы вы отметить его таким образом?

LaKraven 12.12.2011 23:08
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
33
1
20 781
11

Ответы 11

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

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

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

Не могли бы вы пояснить, что подразумевается под «как бы вы скрыли этот метод и не смогли бы вызвать унаследованный»?

Frank 12.09.2008 05:26

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

Martin Liesén 27.09.2008 03:34

@mliesen Если вы не хотите, чтобы вызовы базового класса попадали в ваш «повторно введенный» метод - просто используйте другое имя метода. Вы не «решите» проблему банальный, разорвав цепочку наследования.

Disillusioned 26.09.2016 09:46

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

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

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

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

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

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

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

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

Вы сказали мне, что делает ключевое слово reintroduce. По сути, вы сказали, что это означает, что функция не виртуальная. Да, но при этом к функции не добавляются модификаторы virtual / overide / dynamic. Но почему Андерс Хейлсберг решил, что необходимо добавить в язык ключевое слово reintroduce?

Frank 27.09.2008 02:06

Приятно иметь возможность компиляции уводить вас от потенциальных ошибок. Но с таким же успехом об этом можно было сообщить как о «предупреждении», а не об ошибке.

Martin Liesén 27.09.2008 03:28

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

Я не вижу reintroduce в TComponent.Create.

Rob Kennedy 01.07.2013 22:53

@RobKennedy Ага, не нужно здесь повторно вводить, хотя пример неверен, но основной. Интересно, есть ли какая-то магия компилятора, которая подавляет это предупреждение при наследовании от TObject.

Alister 09.02.2015 00:02

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

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

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

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

Здесь есть много ответов о том, почему компилятор, позволяющий скрыть функцию-член тихо, - плохая идея. Но ни один современный компилятор тихо не скрывает функции-члены. Даже в 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 был разработан таким образом, чтобы программист был осторожно убежден, генерируя предупреждение, явно заявить о своих намерениях в таких ситуациях.

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

Peter Turner 27.09.2008 22:39

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

Mason Wheeler 23.12.2008 20:10

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

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

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

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

Reintroduce не разрывает цепочку наследования. Он подавляет предупреждение о, нарушающее цепочку наследования.

Rob Kennedy 01.07.2013 22:03

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

Alister 09.02.2015 00:09

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

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

Rob Kennedy 01.07.2013 21:57

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

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

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

Или вместо использования повторно ввести, чтобы зарыть голову в песок: просто переименуйте свой метод, чтобы больше не нарушать цепочку наследования. (Без инструмента рефакторинга вы можете использовать поиск и замену в крайнем случае.)

Disillusioned 26.09.2016 09:38

@CraigYoung, который очень хорошо работает с новой или кодовой базой и может работать с меньшими существующими кодовыми базами, но часто создает огромные проблемы с более крупными существующими кодовыми базами.

Jeroen Wiert Pluimers 26.09.2016 10:21

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

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

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 больше не может вести себя так, как она была спроектирована; особенно с подклассами ваших компонентов.

Disillusioned 26.09.2016 09:40

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

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;

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

Другие вопросы по теме