隐藏基类的虚方法有什么问题?

时间:2017-05-26 22:27:55

标签: oop delphi inheritance polymorphism

我一直在收到有关Method 'Create' hides virtual method of base.

的Delphi编译器警告

我已经回顾了几个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?)中写道,多态性是:“面向对象编程的概念。不同对象以自己的方式响应相同的消息的能力被称为多态。” 所以我提出了一个不同的接口,即不再是相同的消息,从而打破了多态性?

我错过了什么?总之,在我的示例中,不是隐藏基本代码是好事吗?

2 个答案:

答案 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是另一个经常从基类调用而不是派生的过程(即:我们不想关注实现,我们只想复制一个对象,无论它是什么)。例如,TMemoLines: 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,这就像魅力一样。

您的计划中需要继承的这一方面取决于您。如果再有许多共同代码(一个涉及机器而另一个涉及汽车)或者有很多条件,那么就会出现问题,一些好的多态可能会有所帮助。