reintroduce
在 Delphi中使用关键字的动机是什么?
如果您有一个子类包含一个与父类中的虚函数同名的函数,并且没有使用 override 修饰符声明它,那么这是一个编译错误。在这种情况下添加 reintroduce 修饰符可以修复错误,但我从未掌握编译错误的原因。
reintroduce
在 Delphi中使用关键字的动机是什么?
如果您有一个子类包含一个与父类中的虚函数同名的函数,并且没有使用 override 修饰符声明它,那么这是一个编译错误。在这种情况下添加 reintroduce 修饰符可以修复错误,但我从未掌握编译错误的原因。
如果您在后代类中声明一个与祖先类中的方法同名的方法,那么您将隐藏该祖先方法——这意味着如果您有该后代类的实例(被引用为该类),那么您将没有得到祖先的行为。当祖先的方法是虚拟或动态时,编译器会给你一个警告。
现在您有两种选择之一来抑制该警告消息:
因此覆盖和重新引入之间的区别在于多态性。使用reintroduce,如果您将后代对象转换为父类型,然后调用该方法,您将获得祖先方法,但如果您访问后代类型,那么您将获得后代的行为。使用覆盖你总是得到后代。如果祖先方法既不是virtual也不是dynamic,则reintroduce不适用,因为该行为是隐式的。(实际上你可以使用类助手,但我们现在不会去那里。)
尽管 Malach 说了什么,您仍然可以在重新引入的方法中调用继承,即使父级既不是virtual也不是dynamic。
本质上 reintroduce 就像override,但它适用于非动态和非虚拟方法,并且如果通过祖先类型的表达式访问对象实例,它不会替换行为。
进一步说明:
Reintroduce是一种向编译器传达您没有出错的意图的方式。我们使用override关键字覆盖祖先中的方法,但它要求祖先方法是virtual或dynamic,并且您希望在对象作为祖先类访问时改变行为。现在输入reintroduce。它可以让您告诉编译器您没有意外创建与虚拟或动态祖先方法同名的方法(如果编译器没有警告您,这将很烦人)。
这里有很多关于为什么让你隐藏成员函数的编译器是一个坏主意的答案。但是没有现代编译器会默默地隐藏成员函数。即使在允许这样做的 C++ 中,也总会有一个警告,这应该就足够了。
那么为什么需要“重新引入”呢?主要原因是,当您不再查看编译器警告时,这种错误实际上可能会意外出现。例如,假设您从 TComponent 继承,Delphi 设计者向 TComponent 添加了一个新的虚函数。坏消息是您五年前编写并分发给其他人的派生组件已经具有该名称的功能。
如果编译器刚刚接受了这种情况,一些最终用户可能会重新编译您的组件,忽略警告。奇怪的事情会发生,你会受到指责。这要求他们明确接受该功能不是同一个功能。
RTL 使用 reintroduce 来隐藏继承的构造函数。例如, TComponent 有一个构造函数,它接受一个参数。但是,TObject 有一个无参数的构造函数。RTL 希望您在实例化新的 TComponent 时仅使用 TComponent 的单参数构造函数,而不是从 TObject 继承的无参数构造函数。所以它使用 reintroduce 来隐藏继承的构造函数。这样,reintroduce 有点像在 C# 中将无参数构造函数声明为私有。
首先,“reintroduce”打破了继承链,不应该使用,我的意思是never ever。在我与 Delphi 合作的整个过程中(大约 10 年),我偶然发现了许多使用这个关键字的地方,而且它一直是设计中的一个错误。
考虑到这一点,这是它最简单的工作方式:
就像我说的那样,这是纯粹的邪恶,必须不惜一切代价避免(嗯,至少这是我的观点)。这就像使用goto - 只是一种糟糕的风格:D
tl; dr:试图覆盖非虚拟方法是没有意义的。添加关键字reintroduce
以确认您犯了一个错误。
reintroduce 修饰符的目的是防止常见的逻辑错误。
我将假设 reintroduce 关键字如何修复警告是众所周知的,并将解释为什么会生成警告以及为什么该关键字包含在语言中。考虑下面的德尔福代码;
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 形成鲜明对比,在 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,无论他们做什么,它都不会被调用。当然,这可能是灾难性的。
因此,Delphi 中的对象模型需要在子类链中显式声明虚函数。这样做的副作用是很容易忘记在子方法上添加覆盖修饰符。Reintroduce 关键字的存在为程序员提供了便利。Delphi 的设计目的是通过生成警告来温和地说服程序员明确说明他们在这种情况下的意图。
当祖先类也有一个同名的方法,并且它不一定被声明为虚拟时,你会看到一个编译器警告(因为你会隐藏这个方法)。
换句话说:你告诉编译器你知道你隐藏了祖先函数并用这个新函数替换它,并且故意这样做。
你为什么要这样做?如果该方法在父类中是虚的,唯一的原因是为了防止多态。除此之外,只是覆盖而不调用继承。但是如果父方法没有声明为虚拟的(并且你不能改变它,因为你不拥有例如代码),你可以从那个类继承并让人们从你的类继承而不会看到编译器警告。
Reintroduce 告诉编译器您要调用此方法中定义的代码作为此类及其后代的入口点,而不管祖先链中具有相同名称的其他方法。
创建 aTDescendant.MyMethod
会给 TDescendants 添加另一个具有相同名称的方法带来潜在的混淆,编译器会警告您。
Reintroduce 消除了歧义,并告诉编译器您知道使用哪一个。
ADescendant.MyMethod
称 TDescendant 为一,(ADescendant as TAncestor).MyMethod
称 Tancestor 为一。总是!没有混乱…… 编译开心!
无论您是否希望后代方法是虚拟的,这都是正确的:在这两种情况下,您都希望打破虚拟链的自然链接。它不会阻止您从新方法中调用继承的代码。
由于框架版本(包括 VCL),这已被引入该语言。
如果您有一个现有的代码库,并且对框架的更新(例如,因为您购买了较新的 Delphi 版本)引入了一个与您的代码库的祖先中的方法同名的虚拟方法,那么reintroduce
将允许您获得摆脱W1010 警告。
这是您应该使用的唯一地方reintroduce
。
首先,正如上面所说,你永远不应该故意重新引入虚拟方法。唯一合理的使用 reintroduce 是祖先的作者(不是您)添加了一个与您的后代冲突的方法并且重命名您的后代方法不是一种选择。其次,即使在使用不同参数重新引入虚拟方法的类中,您也可以轻松调用原始版本的虚拟方法:
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;
那么除了让事情更难阅读之外,重新引入虚拟方法的目的应该是什么?
reintroduce允许你声明一个与祖先同名但参数不同的方法。它与错误或错误无关!
例如,我经常将它用于构造函数......
constructor Create (AOwner : TComponent; AParent : TComponent); reintroduce;
这使我能够以更简洁的方式为工具栏或日历等复杂控件创建内部类。我通常有比这更多的参数。有时在不传递一些参数的情况下创建一个类几乎是不可能或非常混乱的。
对于可视化控件,Application.Processmessages 可以在 Create 之后调用,这可能为时已晚,无法使用这些参数。
constructor TClassname.Create (AOwner : TComponent; AParent : TComponent);
begin
inherited Create (AOwner);
Parent := AParent;
..
end;