重新介绍Delphi中的函数

36

在 Delphi 中引入 reintroduce 关键字的动机是什么?

如果您有一个子类包含一个与父类中虚函数同名的函数,且未使用 override 修饰符声明,则会导致编译错误。在这种情况下添加 reintroduce 修饰符可以解决错误,但我从未理解编译错误的原因。


7
这个问题的最佳答案确实是正确的,请您标记它为最佳答案,谢谢。 - LaKraven
https://dev59.com/5nRB5IYBdhLWcg3wCjcV - Gabriel
11个回答

72
如果你在一个派生类中声明了与祖先类中的方法同名的方法,那么你就隐藏了祖先方法——这意味着如果你有一个该派生类的实例(引用为该类),则你将无法得到祖先的行为。当祖先的方法是虚拟或动态的时,编译器会给出一个警告。
现在你有两个选择来抑制该警告信息:
1.添加关键字reintroduce只是告诉编译器你知道你隐藏了那个方法,并且它会抑制警告。您仍然可以在那个下降方法的实现中使用inherited关键字来调用祖先方法。
2.如果祖先方法是虚拟或动态的,则可以使用override。它具有以下行为:如果通过祖先类型的表达式访问此下降对象,则对该方法的调用仍将是对下降方法的调用(然后可以选择通过inherited调用祖先)。
因此,overridereintroduce之间的差异在于多态性。使用reintroduce,如果将下降对象转换为父类型,然后调用该方法,您将获得祖先方法,但如果以下降类型访问它,则将获取下降的行为。使用override,您始终会获得下降的行为。如果祖先方法既不是virtual也不是dynamic,则reintroduce不适用,因为该行为是隐含的。(实际上你可以使用一个类辅助器,但我们现在不去那里)
尽管Malach说过,你仍然可以在重新引入的方法中调用inherited,即使父类既不是虚拟的也不是动态的。

重新引入(reintroduce)本质上就像是override,但它适用于非dynamic和非virtual方法,并且如果通过祖先类型的表达式访问对象实例,则不会替换行为。

更进一步的解释:

重新引入是一种向编译器传达意图的方式,即您没有犯错误。我们使用override关键字覆盖祖先中的方法,但它要求祖先方法是virtualdynamic的,并且当以祖先类的形式访问对象时,您希望行为发生变化。现在介绍重新引入。它允许您告诉编译器,您并没有意外创建与虚拟或动态祖先方法同名的方法(如果编译器没有警告您,这将很麻烦)。


你告诉我了reintroduce关键字的作用。基本上,你说它意味着函数不是虚拟的。是的,但是如果一个函数没有添加任何virtual/override/dynamic修饰符,也是如此。但是为什么Anders Hejlsberg决定将reintroduce关键字添加到语言中呢? - Frank
1
编译器能够帮助我们避免潜在的错误,这是非常好的。但是,它也可以将其报告为“警告”,而不是错误。 - Martin Liesén

8
这里有很多关于为什么允许隐藏成员函数是个坏主意的答案。但是现代编译器都不会悄悄地隐藏成员函数了。即使在C++中,它允许这样做,但总会有一个警告,而这应该足够了。
那么为什么需要 "reintroduce" 呢?主要原因是这种 bug 可能会在你不再查看编译器警告时意外出现。例如,假设你从 TComponent 继承,并且 Delphi 设计师添加了一个新的虚拟函数到 TComponent 中。坏消息是你编写并分发给其他人的派生组件已经有了同名的函数,而且已经五年了。
如果编译器接受了这种情况,某些最终用户可能会重新编译您的组件,忽略警告。奇怪的事情会发生,而你会受到责备。这要求他们明确接受该函数不是相同的函数。

4
首先,“reintroduce”破坏了继承链,不应该使用,我的意思是永远不要使用。在我使用Delphi(约10年)的整个时间里,我遇到过一些使用这个关键字的地方,但这在设计上总是错误的。
考虑到这一点,以下是最简单的工作原理:
1. 在基类中有一个虚方法 2. 现在你想要一个方法,它有完全相同的名称,但可能具有不同的签名。所以你在派生类中写入同名的方法,但因为契约没有得到履行,所以无法编译。 3. 你在这里放置“reintroduce”关键字,你的基类不知道你的全新实现,你只能从直接指定的实例类型访问它。这意味着你不能仅仅将对象分配给基本类型的变量并调用该方法,因为由于契约已被破坏,该方法不存在。
就像我说的,这纯粹是邪恶的,必须尽一切努力避免使用(好吧,至少这是我的观点)。这就像使用“goto”-一种可怕的风格:D

1
“Reintroduce” 不会破坏继承链,它只是抑制了关于破坏继承链的警告。 - Rob Kennedy
1
我认为这里的例外是构造函数,它允许您使用参数重新引入构造函数 - 否则,您将无法这样做而不会收到警告(除非从没有构造函数的类树继承)。 - Alister

4

RTL使用reintroduce来隐藏继承的构造函数。例如,TComponent有一个带一个参数的构造函数。但是,TObject有一个无参构造函数。当实例化一个新的TComponent时,RTL希望您仅使用TComponent的带有一个参数的构造函数,而不是从TObject继承的无参构造函数。因此,它使用reintroduce来隐藏继承的构造函数。这种方式,reintroduce有点像在C#中将无参构造函数声明为私有。


我在TComponent.Create中没有看到reintroduce - Rob Kennedy
@RobKennedy 没错,这里没有重新引入,尽管示例是错误的,但原则是正确的。我想知道是否有一些编译器魔法可以在从TObject继承时抑制此警告。 - Alister

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 Warning] Project9.dpr(19): W1010 Method 'Procedure1' hides virtual method of base type '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毫无了解,因此他们肯定永远不会调用inherited。因此,如果库设计者打算调用TParent.Setup,无论他们做什么,都不会被调用。这肯定可能是灾难性的。
因此,在Delphi中的对象模型要求在子类的链中明确声明虚拟函数。这个的副作用是很容易忘记在子方法上添加override修饰符。Reintroduce关键字的存在是程序员的便利。Delphi的设计是让程序员通过生成警告来温和地说服他们在这种情况下明确陈述他们的意图。

你已经非常简洁地回答了自己的问题,但是为什么 reintroduce 是一个关键字而不是编译器选项,尽管它的用处非常微小? - Peter Turner
1
因为当它有用时,它非常有用。可以防止某些东西意外损坏。 - Mason Wheeler

3

简而言之:试图覆盖非虚方法是没有意义的。添加关键字reintroduce来承认您犯了一个错误。


3
试图覆盖非虚方法始终是错误的。没有额外的指令可以使编译器改变这一点。 - Rob Kennedy

2
当祖先类也有同名方法时,即使它没有被声明为虚方法,你也会看到编译器警告(因为你隐藏了这个方法)。
换句话说:你告诉编译器,你知道你隐藏了祖先函数并用这个新函数替换它,并且是故意这么做的。
为什么要这样做呢?如果父类中的方法是虚方法,唯一的原因就是防止多态。除此之外,只需覆盖而不调用继承。但是,如果父方法没有声明为虚方法(例如,你无法更改代码,因为你不拥有它),你可以从该类继承,并让人们从你的类继承而不看到编译器警告。

你能否澄清一下“就像你要隐藏这个方法并且不能调用继承”的意思是什么? - Frank
重新引入方法而非覆盖原有方法是有充分的理由的。因为你的父类可能会调用虚方法,但你又不想让这些调用传递到重新引入的方法中。 - Martin Liesén
@mliesen 如果您不希望基类调用到您的“重新引入”方法 - 简单地使用不同的方法名称即可。您不应该通过打破继承链来“解决”一个微不足道的问题。 - Disillusioned

2
重新引入(Reintroduce)告诉编译器,你想调用在该方法中定义的代码作为该类及其子类的入口点,而不管祖先链中其他具有相同名称的方法。创建TDescendant.MyMethod会对TDescendants造成潜在的混淆,因为它会添加另一个同名方法,编译器会警告你。重新引入消除了这种歧义,并告诉编译器你知道要使用哪个方法。ADescendant.MyMethod调用TDescendant的方法,(ADescendant as TAncestor).MyMethod调用TAncestor的方法。总是如此!没有混淆...编译器很开心!
无论您是否希望后代方法是虚拟的,这都是正确的:在两种情况下,您都希望打破虚拟链的自然链接。并且它不会阻止您从新方法中调用继承的代码。
  1. TDescendant.MyMethod是虚拟的:但您不能或不想使用链接。
    • 你不能,因为方法签名不同。在这种情况下,如果返回类型或参数不完全相同,则无法覆盖,因此你别无选择。
    • 您希望从该类重新启动继承树。
  2. 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

这是由于框架版本(包括VCL)而引入语言的。

如果您有现有的代码库,并且框架更新(例如因为您购买了更新的Delphi版本)引入了与代码库祖先中的方法同名的虚拟方法,那么reintroduce将允许您摆脱W1010警告

这是您唯一应该使用reintroduce的地方。


或者,不要使用“重新引入”来掩耳盗铃:只需重命名您的方法,这样您就不会再破坏继承链了。(如果没有重构工具,您可以在紧急情况下使用搜索和替换。) - Disillusioned
@CraigYoung 这在新的或代码库中非常有效,也可以在较小的现有代码库中工作,但在较大的现有代码库中经常会带来巨大的头痛。 - Jeroen Wiert Pluimers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接