在Delphi中重新引入功能
在Delphi中使用reintroduce
关键字的动机是什么?
如果子类包含与父类中的虚拟函数同名的函数,并且未使用override修饰符声明该子函数,则这是编译错误。在这种情况下,添加重新引入修饰符可以修复该错误,但是我从未掌握过编译错误的原因。
解决方案
回答
当祖先类也有一个同名的方法,并且不一定要声明为virtual时,我们会看到编译器警告(因为我们将隐藏此方法)。
换句话说:我们告诉编译器我们知道自己隐藏了祖先函数,并用此新函数替换了它,并有意这么做。
那你为什么要这么做呢?如果该方法在父类中是虚拟的,则唯一的原因是防止多态。否则,仅重写而不调用继承。但是,如果未将父方法声明为虚拟方法(并且我们无法更改它,因为例如我们不拥有代码),则可以从该类继承并让人们从类继承而不会看到编译器警告。
回答
如果我们在后代类中声明的方法与祖先类中的方法具有相同的名称,则我们将隐藏该祖先方法,这意味着如果我们具有该后代类的实例(被称为该类),则不会得到祖先的行为。当祖先的方法是虚拟的或者动态的时,编译器将向我们发出警告。
现在,我们可以选择以下两种选择之一来禁止显示该警告消息:
- 添加关键字reintroduce只会告诉编译器我们知道我们正在隐藏该方法,并且可以抑制警告。我们仍然可以在该后代方法的实现中使用Inherited关键字来调用祖先方法。
- 如果祖先的方法是虚拟的或者动态的,则可以使用覆盖。它具有添加的行为,即如果通过后代类型的表达式访问此后代对象,则对该方法的调用仍将是后代方法(然后可以选择通过继承调用该后代)。
因此,覆盖和重新引入之间的差异在于多态性。使用reintroduce,如果将后代对象转换为父类型,则调用该方法将获得祖先方法,但是如果访问它的后代类型,则将获得后代的行为。使用override,我们总是可以得到后代。如果祖先方法既不是虚拟方法也不是动态方法,则重新引入将不适用,因为该行为是隐式的。 (实际上,我们可以使用课程帮手,但我们现在不会去那里。)
尽管Malach说了什么,我们仍然可以在重新引入的方法中调用继承,即使父代既不是虚拟的也不是动态的。
本质上,重新引入就像重写一样,但是它与非动态和非虚拟方法一起使用,并且如果通过祖先类型的表达式访问对象实例,则它不会替代行为。
进一步说明:
重新引入是一种向编译器传达我们没有出错的意图的方法。我们使用override关键字覆盖了祖先中的方法,但是它要求祖先方法是虚拟的或者动态的,并且我们希望当对象作为祖先类被访问时行为发生变化。现在输入重新引入。它使我们可以告诉编译器我们并非偶然创建了一个与虚拟或者动态祖先方法同名的方法(如果编译器没有警告我们,这将很烦人)。
回答
RTL使用重新引入来隐藏继承的构造函数。例如,TComponent具有一个带有一个参数的构造函数。但是,TObject具有无参数的构造函数。 RTL希望我们在实例化新的TComponent时仅使用TComponent的单参数构造函数,而不使用从TObject继承的无参数构造函数。因此,它使用重新引入来隐藏继承的构造函数。这样,重新引入有点像在C#中将无参数构造函数声明为私有。
回答
重新引入告诉编译器我们要调用此方法中定义的代码作为此类及其后代的入口点,而与祖先链中具有相同名称的其他方法无关。
创建TDescendant.MyMethod
会给TDescendants带来潜在的困惑,即添加另一个具有相同名称的方法,编译器会警告我们。
重新引入消除歧义,并告诉编译器我们知道要使用哪个编译器。ADescendant.MyMethod
称为TDescendant,(ADescendant as TAncestor).MyMethod
称为TAncestor。总是!不会造成混乱。编译快乐!
无论我们希望后代方法是否为虚拟方法,这都是正确的:在两种情况下,我们都希望打破虚拟链的自然联系。
并且它不会阻止我们从新方法中调用继承的代码。
- 我们想从此类重新启动继承树。
- TDescendant.MyMethod不是虚拟的:在TDescendant级别将MyMethod转换为静态方法,并防止进一步的覆盖。从TDescendant继承的所有类都将使用TDescendant实现。
回答
这里有很多答案,说明为什么让我们静默隐藏成员函数的编译器不是一个好主意。但是,没有现代编译器会默默地隐藏成员函数。即使在允许这样做的C ++中,也总是会有关于它的警告,这应该足够了。
那么为什么要"重新引入"呢?主要原因是当我们不再查看编译器警告时,这种错误实际上可能偶然出现。例如,假设我们从TComponent继承,而Delphi设计人员向TComponent添加了新的虚函数。坏消息是衍生组件,该组件是我们五年前编写并分发给他人的,已经具有该名称的功能。
如果编译器刚刚接受这种情况,则某些最终用户可能会重新编译组件,请忽略该警告。奇怪的事情会发生,而你会受到责备。这就要求他们明确接受该功能不是同一功能。
回答
重新引入修饰符的目的是防止出现常见的逻辑错误。
我将假定重新引入关键字如何解决警告是众所周知的知识,并解释为什么会生成警告以及为什么该关键字包含在语言中。考虑下面的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相反,在Java中,向类的父类添加新功能充满了危险,因为类是隐式虚拟的。可以说,上面的TParent位于第3方库中,并且库制造商在下面发布了新版本。
// 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;
如果使用TChild参数调用TestLibrary,则一切都会按预期进行。库设计者不了解TChild.Setup,在Delphi中这不会造成任何危害。上面的调用正确解析为TParent.Setup。
在Java的等效情况下会发生什么? TestClient将按预期正常工作。 TestLibrary不会。在Java中,所有功能均假定为虚拟的。 Parent.Setup将解析为TChild.Setup,但请记住,编写TChild.Setup时他们不知道将来的TParent.Setup,因此,它们肯定不会调用继承的。因此,如果库设计人员希望调用TParent.Setup,则无论如何都不会调用TParent.Setup。当然,这可能是灾难性的。
因此,Delphi中的对象模型需要在子类链的下游显式声明虚拟函数。这样做的副作用是,很容易忘记在子方法上添加override修饰符。 Reintroduce关键字的存在为程序员提供了便利。 Delphi的设计旨在通过发出警告来温和地劝说程序员在这种情况下明确说明其意图。
回答
首先,"重新引入"会破坏继承链,因此不应使用,我的意思是永远不会。在与Delphi合作的整个过程中(大约10年),我偶然发现了许多使用此关键字的地方,这在设计中一直是错误的。
考虑到这一点,这是最简单的工作方式:
- 我们就像基类中的虚拟方法一样
- 现在,我们想要一个名称完全相同但签名可能不同的方法。因此,我们用相同的名称在派生类中编写方法,由于未履行合同,该方法将无法编译。
- 我们在其中放置了reintroduce关键字,而基类不知道全新实现,并且仅当从直接指定的实例类型访问对象时才可以使用它。这意味着玩具不能仅仅将对象分配给基本类型的变量并调用该方法,因为它与损坏的合同不存在。
就像我说的那样,这是纯粹的邪恶,必须不惜一切代价避免(好吧,至少我是这样认为的)。就像使用goto只是一种糟糕的风格:D