在Delphi中使用reintroduce关键字的动机是什么?
如果您的子类包含与父类中的虚拟函数同名的函数,并且未使用override修饰符声明该子函数,则这是编译错误。 在这种情况下添加reintroduce修饰符可以修复该错误,但是我从未掌握过编译错误的原因。
如果您在后代类中声明的方法与祖先类中的方法具有相同的名称,那么您将隐藏该祖先方法-这意味着,如果您具有该后代类的实例(被称为该类),则您将不了解祖先的行为。当祖先的方法是虚拟的或动态的时,编译器将向您发出警告。
现在,您有两种选择可禁止显示该警告消息:
添加关键字reintroduce只会告诉编译器您知道您正在隐藏该方法,并且可以抑制警告。您仍然可以在该后代方法的实现中使用Inherited关键字来调用祖先方法。
如果祖先的方法是虚拟的或动态的,则可以使用覆盖。它具有附加的行为,即如果通过后代类型的表达式访问此后代对象,则对该方法的调用仍将是后代方法(然后可以选择通过继承调用该后代)。
因此,覆盖和重新引入之间的差异在于多态性。使用reintroduce,如果将后代对象强制转换为父类型,则调用该方法将获得祖先方法,但是如果访问它的后代类型,则将获得后代的行为。使用override,您总是可以得到后代。如果祖先方法既不是虚拟方法也不是动态方法,则重新引入将不适用,因为该行为是隐式的。 (实际上,您可以使用课程帮手,但我们现在不会去那里。)
尽管Malach说了什么,您仍然可以在重新引入的方法中调用继承,即使父代既不是虚拟的也不是动态的。
本质上,重新引入就像重写一样,但是它与非动态和非虚拟方法一起使用,并且如果通过祖先类型的表达式访问对象实例,则它不会替代行为。
进一步说明:
重新引入是一种向编译器传达您没有犯错误的意图的方法。我们使用override关键字覆盖了祖先中的方法,但是它要求祖先方法是虚拟的或动态的,并且您希望在访问对象作为祖先类时更改行为。现在输入重新介绍。它可以让您告诉编译器您并非偶然创建了一个与虚拟或动态祖先方法同名的方法(如果编译器没有警告您,这将很烦人)。
这里有很多答案,说明为什么让您静默隐藏成员函数的编译器是个坏主意。但是,没有现代编译器会默默地隐藏成员函数。即使在允许这样做的C ++中,也总是会有关于它的警告,这应该足够了。
那么为什么要"重新引入"呢?主要原因是当您不再查看编译器警告时,这种错误实际上可能偶然出现。例如,假设您从TComponent继承,而Delphi设计人员向TComponent添加了新的虚函数。坏消息是您的派生组件,该组件是您五年前编写并分发给他人的,已经具有该名称的功能。
如果编译器刚刚接受这种情况,则某些最终用户可能会重新编译您的组件,请忽略该警告。奇怪的事情会发生,而你会受到责备。这要求他们明确接受该功能不是同一功能。
RTL使用重新引入来隐藏继承的构造函数。例如,TComponent有一个带有一个参数的构造函数。但是,TObject具有无参数的构造函数。 RTL希望您在实例化新的TComponent时仅使用TComponent的单参数构造函数,而不使用从TObject继承的无参数构造函数。因此,它使用重新引入来隐藏继承的构造函数。这样,重新引入有点像在C#中将无参数构造函数声明为私有。
tl; dr:尝试覆盖非虚拟方法没有任何意义。添加关键字reintroduce以确认您犯了一个错误。
首先,"重新引入"打破了继承链,不应使用,我的意思是永远不会。在与Delphi合作的整个过程中(大约10年),我偶然发现了许多使用此关键字的地方,这在设计中一直是错误的。
考虑到这一点,这是最简单的工作方式:
您就像基类中的虚拟方法一样
现在,您想要一个名称完全相同但签名可能不同的方法。因此,您用相同的名称在派生类中编写方法,由于未履行合同,该方法将无法编译。
您在其中放置了reintroduce关键字,并且基类不知道您的全新实现,并且仅当从直接指定的实例类型访问对象时才可以使用它。这意味着玩具不能仅将对象分配给基本类型的变量并调用该方法,因为它与损坏的合同不存在。
就像我说的那样,这是纯粹的邪恶,必须不惜一切代价避免(好吧,至少我是这样认为的)。就像使用goto-只是一种糟糕的风格:D
重新引入修饰符的目的是防止出现常见的逻辑错误。
我将假定重新引入关键字如何解决警告是众所周知的知识,并解释为什么会生成警告以及为什么将关键字包含在语言中。考虑下面的delphi代码;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| 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方库中,并且库制造商在下面发布了新版本。
1 2 3 4 5 6 7 8 9 10 11 12 13
| // 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; |
假设我们的客户代码中包含以下代码
1 2 3 4 5 6 7
| Procedure TestClient;
Var
Child : TChild;
Begin
Child := TChild.Create;
Child.Setup;
End; |
对于客户端而言,代码是针对库的版本2还是版本1编译都没有关系,在两种情况下,都按照用户的意图调用TChild.Setup。在图书馆里
1 2 3 4 5
| // 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的设计旨在通过发出警告来温和地劝说程序员在这种情况下明确说明其意图。
重新引入告诉编译器您要调用此方法中定义的代码作为该类及其后代的入口点,而与祖先链中具有相同名称的其他方法无关。
创建TDescendant.MyMethod可能会给TDescendants带来潜在的混乱,即添加另一个具有相同名称的方法,编译器会警告您。
重新引入消除歧义,并告诉编译器您知道要使用哪个编译器。
ADescendant.MyMethod称为TDescendant,(ADescendant as TAncestor).MyMethod称为TAncestor。总是!不要混淆...。编译快乐!
无论您希望后代方法是否为虚拟方法都是如此:在两种情况下,您都希望打破虚拟链的自然联系。
并且它不会阻止您从新方法中调用继承的代码。
TDescendant.MyMethod是虚拟的:...但是您不能或不想使用链接。
-
您不能因为方法签名不同。您别无选择,因为在这种情况下,如果返回类型或参数不完全相同,则无法进行覆盖。
-
您想从此类重新启动继承树。
TDescendant.MyMethod不是虚拟的:在TDescendant级别将MyMethod转换为静态方法,并防止进一步的覆盖。从TDescendant继承的所有类都将使用TDescendant实现。
当祖先类也有一个同名的方法,并且不一定要声明为virtual时,您将看到编译器警告(因为您将隐藏此方法)。
换句话说:您告诉编译器您知道自己隐藏了祖先函数,并用此新函数替换了它,并有意这么做。
为什么要这么做?如果该方法在父类中是虚拟的,则唯一的原因就是要防止多态。否则,仅重写而不调用继承。但是,如果未将父方法声明为虚拟方法(并且您无法更改它,因为例如您不拥有代码),则可以从该类继承并让人们从您的类继承而不会看到编译器警告。
首先,如上所述,您永远不要故意重新引入虚拟方法。重新引入的唯一明智的用法是,祖先(不是您)的作者添加了与后代发生冲突的方法,并且重命名后代方法不是一种选择。其次,您甚至可以在使用不同参数重新引入虚拟类的类中轻松调用虚拟方法的原始版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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允许您声明与祖先名称相同但参数不同的方法。它与错误或错误无关!!!
例如,我经常将其用于构造函数...
1
| constructor Create (AOwner : TComponent; AParent : TComponent); reintroduce; |
这使我能够以更简洁的方式为复杂的控件(例如工具栏或日历)创建内部类。我通常有更多的参数。有时,在不传递某些参数的情况下创建类几乎是不可能的,也可能会很混乱。
对于可视控件,可以在Create之后调用Application.Processmessages,这对于使用这些参数可能为时已晚。
1 2 3 4 5 6
| constructor TClassname.Create (AOwner : TComponent; AParent : TComponent);
begin
inherited Create (AOwner);
Parent := AParent;
..
end; |