我一直在收到有关Method 'Create' hides virtual method of base.
我已经回顾了几个Stack Overflow链接(见下文),我不明白这个警告背后的逻辑,以及为什么它被认为是错误的编码实践。我希望别人可以帮助我理解
我将包含一些示例代码:
type
TMachine = class(TPersistent)
private
public
Horsepower : integer;
procedure Assign(Source : TMachine);
end;
...
procedure TMachine.Assign(Source : TMachine);
begin
inherited Assign(Source);
Self.Horsepower := Source.HorsePower;
end;
这会导致编译器警告。
[dcc32 Warning] Unit1.pas(21): W1010 Method 'Assign' hides virtual method of base type 'TPersistent'
我一直忽视这个警告,因为它对我没有任何意义。但这让我以另一种方式陷入困境(请参阅我在这里的另一篇文章:Why does Delphi call incorrect constructor during dynamic object creation?)所以我决定尝试更好地理解这一点。
我知道如果我使用保留字reintroduce
,错误就会消失,但我已经看到它反复发布这是一个坏主意。正如Warren P在这里写的那样(Delphi: Method 'Create' hides virtual method of base - but it's right there),“恕我直言,如果你需要重新引入,你的代码闻起来很可怕”。
我想我明白“隐藏”是什么意思。正如David Heffernan在这里所说的那样(What causes "W1010 Method '%s' hides virtual method of base type '%s'" warning?):
隐藏的含义是,从派生类中,您不再能够访问基类中声明的虚方法。您不能引用它,因为它与派生类中声明的方法具有相同的名称。后一种方法是从派生类中可见的方法。
但我有点困惑,因为似乎祖先方法并没有真正隐藏,因为派生类总是可以使用inherited
关键字来调用基类中的方法。所以'隐藏'真的意味着'有点隐藏'吗?
我想我也明白使用保留字override
会阻止编译器警告,但过程签名必须相同(即没有新添加的参数)。我不能在这里使用它。
我不明白为什么隐藏是值得警告的。在上面的代码示例中,我不希望TMachine.Assign()
的用户以某种方式使用TPersistent.Assign()
。在我的扩展课程中,我扩展了需求,因此希望他们使用新的和改进的功能。因此,隐藏旧代码似乎正是我想要的。我对virtual
方法的理解是基于运行时对象的实际类型调用正确方法的方法。在这种情况下,我认为不应该有任何影响。
附加代码,将添加到
上面的示例代码中 TAutomobile = class(TMachine)
public
NumOfDoors : integer;
constructor Create(NumOfDoors, AHorsepower : integer);
end;
...
constructor TAutomobile.Create(ANumOfDoors, AHorsepower : integer);
begin
Inherited Create(AHorsepower);
NumOfDoors := ANumOfDoors;
end;
这会添加新的编译器警告消息:[dcc32 Warning] Unit1.pas(27): W1010 Method 'Create' hides virtual method of base type 'TMachine'
我特别不理解使用带有附加参数的新构造函数时出现的问题。在这篇文章(SerialForms.pas(17): W1010 Method 'Create' hides virtual method of base type 'TComponent')中,智慧似乎应该引入具有不同名称的构造函数,例如CreateWithSize
。这似乎允许用户选择他们想要使用的构造函数。
如果他们选择旧的构造函数,扩展类可能会缺少一些创建所需的信息。但是,如果相反,我“隐藏”了先前的构造函数,那就是编程错误。 Marjan Venema在同一链接中写了reintroduce
:重新引入中断多态性。这意味着您不能再使用元类(TxxxClass = Tyyy类)来实例化您的TComponent后代,因为它的Create将不会被调用。我根本不理解这一点。
也许我需要更好地理解多态性。 Tony Stark在这个链接(What is polymorphism, what is it for, and how is it used?)中写道,多态性是:“面向对象编程的概念。不同对象以自己的方式响应相同的消息的能力被称为多态。” 所以我提出了一个不同的接口,即不再是相同的消息,从而打破了多态性?
我错过了什么?总之,在我的示例中,不是隐藏基本代码是好事吗?
答案 0 :(得分:6)
这里的危险是你可以在基类引用上调用Assign
。因为您没有使用override
,所以不会调用派生类方法。你已经破坏了多态性。
根据最少惊喜的原则,您应该在此处使用override
,或者为您的派生类方法指定一个不同的名称。后一种选择很简单。前者看起来像这样:
type
TMachine = class(TPersistent)
public
Horsepower : integer;
procedure Assign(Source : TPersistent); override;
end;
...
procedure TMachine.Assign(Source : TPersistent);
begin
if Source is TMachine then begin
Horsepower := TMachine(Source).Horsepower;
end else begin
inherited Assign(Source);
end;
end;
这允许您的班级与TPersistent
的多态设计合作。不使用override
这是不可能的。
下一个示例,虚拟构造函数与此类似。使构造函数成为虚拟的整个过程,以便您可以在运行时直到创建实例而不知道它们的类型。规范示例是流式框架,即处理.dfm / .fmx文件并创建对象并设置其属性的框架。
流式传输框架依赖于TComponent
:
constructor Create(AOwner: TComponent); virtual;
如果希望组件使用流式传输框架,则必须覆盖此构造函数。如果你隐藏它,那么流式框架找不到你的构造函数。
考虑流式传输框架如何实例化组件。它不知道它需要使用的所有组件类。例如,它不能考虑第三方代码,即您编写的代码。 Delphi RTL无法知道那里定义的类型。流式传输框架实例化这样的组件:
type
TComponentClass = class of TComponent;
var
ClassName: string;
ClassType: TComponentClass;
NewComponent: TComponent;
....
ClassName := ...; // read class name from .dfm/.fmx file
ClassType := GetClass(ClassName); // a reference to the class to be instantiated
NewComponent := ClassType.Create(...); // instantiate the component
ClassType
变量包含元类。这允许我们表示直到运行时才知道的类型。我们需要以多态方式调用Create
,以便执行组件构造函数中的代码。除非在声明构造函数时使用override
,否则它不会。
实际上,所有这些归结为多态性。如果您对多态性的理解不如您所建议的那么坚定,那么您将很难理解任何这一点。我认为你的下一步行动是更好地掌握多态性。
答案 1 :(得分:2)
使用继承有不同的好处。在您的示例中,您可以避免一次又一次地编写相同的内容。因此,如果TMachine
已经有Horsepower
字段和一些方法,现在您需要更高级的TAutomobile
NumOfDoors
,那么您可以将其作为TMachine
后代。
如果您现在总是以不同的方式对待它们,即在某些代码中您使用TMachine(machine := TMachine.Create(...)
,machine.Assign(AnotherMachine)
等),而在另一个代码中使用TAutomobile
并且它们永远不会混合使用
那么你没事,你可以忽略这些警告或用reintroduce
“静音”。
但是继承通常还有另一个方面:保持统一的界面,或者有时称为“契约”。将接口与实现分开。
例如,form可以释放属于它的所有对象,无论这些对象是什么,都是因为Destroy
方法被覆盖。表单并不关心您的实现,但它知道:要释放对象,只需调用Destroy
即可。如果您没有覆盖Destroy
,那就非常糟糕:TForm
无法将您称为TMachine.Destroy
。它会将您称为TObject.Destroy
,但它不会导致您TMachine.Destroy
,因此会出现内存泄漏。在大多数情况下,当某些方法没有被覆盖时,这只是因为程序员忘了这么做,因此是一个警告:它非常有用。如果程序员没有忘记它,但这是故意的,则使用reintroduce
关键字。这样程序员就会说:“是的,我知道我做了什么,这是故意的,不要打扰我!”
TPersistent.Assign
是另一个经常从基类调用而不是派生的过程(即:我们不想关注实现,我们只想复制一个对象,无论它是什么)。例如,TMemo
有Lines: TStrings
,但TStrings
是抽象类,而实际实现是TStringList
。因此,当您编写Memo1.Lines.Assign(Memo2.Lines)
时,会使用TStrings.Assign
方法。它可以通过另一种方法实现此赋值:首先清除自身然后逐行添加。某些TStrings
后代可能希望通过一些块数据副本来加速进程。当然,它必须使用精确的Assign(Source:TPersistent)方法并覆盖它,否则永远不会调用它(继承被调用)。
Assign的经典实现是这样的:
procedure TMachine.Assign(Source : TPersistent);
begin
if Source is TMachine then
Horsepower := TMachine(Source).Horsepower
else inherited Assign(Source);
end;
当inherited
不应该被称为第一件事时就是这种情况。这是'最后的手段':如果没有别的帮助,它会被称为最后一次。最后一次尝试:如果您的班级不知道如何分配,那么Source
可能知道如何AssignTo
您的班级?
例如,TBitmap
很久以前就被编码了。之后TPngImage
开发出来与PNG合作。您想将PNG放入位图并写入:Bitmap.Assign(PngImage)
。没办法TBitmap
可能知道如何处理PNG:它当时不存在!但TPngImage
作者知道可能会发生并实现AssignTo
方法,该方法能够将其转换为位图。因此,TBitmap
作为最后一根吸管调用TPersistent.Assign
方法,然后调用TPngImage.AssignTo
,这就像魅力一样。
您的计划中需要继承的这一方面取决于您。如果再有许多共同代码(一个涉及机器而另一个涉及汽车)或者有很多条件,那么就会出现问题,一些好的多态可能会有所帮助。