【问题标题】:What's wrong with hiding virtual method of a base class?隐藏基类的虚方法有什么问题?
【发布时间】:2017-10-27 21:36:32
【问题描述】:

我收到关于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 的文章:Reintroduce 打破了多态性。这意味着您不能再使用元类(TxxxClass = Tyyy 的类)来实例化您的 TComponent 后代,因为它的 Create 不会被调用。我完全不明白这一点。

也许我需要更好地理解多态性。 Tony Stark 在此链接 (What is polymorphism, what is it for, and how is it used?) 中写道,多态性是:“面向对象编程的概念。不同对象以自己的方式响应相同消息的能力称为多态性。” em> 那么我是不是呈现了一个不同的界面,即不再是一个相同的消息,从而破坏了多态性?

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

【问题讨论】:

  • 长话短说,它只是确保您知道自己在做什么。如果这样做,您可以通过在末尾添加reintroduce; 来隐藏它以隐藏警告。我个人不明白为什么有人说reintroduce 是不好的做法,我在 RTL 本身中看到过。
  • @Jerry:在 VCL/RTL 中看到它并不意味着它是好的代码或实践。 Delphi 源代码中有大量错误代码。
  • @Ken 我同意这一点。我仍然没有看到使用reintroduce 的危害,但在某些情况下我别无选择 - 当我确实需要以不同的方式实现某些东西时。在某些构造函数中尤其如此。
  • @Jerry:我没有说使用reintroduce 有什么问题。我只是指出 我在 RTL 本身中看到过 没有意义。 :-)
  • @Jerry 你对 TThread 大错特错。它的构造函数不是虚拟的。

标签: oop delphi inheritance polymorphism


【解决方案1】:

这里的危险是您可能会在基类引用上调用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,否则不会。

真的,所有这些都归结为多态性。如果您对多态性的理解并不牢固,正如您所建议的那样,那么您将很难理解其中的任何一个。我认为您的下一步是更好地了解多态性是什么。

【讨论】:

    【解决方案2】:

    使用继承有不同的好处。在您的示例中,您这样做是为了避免一次又一次地编写相同的代码。所以如果TMachine 已经有Horsepower 字段和一些方法,现在你需要更高级的TAutomobileNumOfDoors,你就让它TMachine 后代。

    如果您现在总是以不同的方式对待它们,即在某些代码中您完全使用 TMachine(machine := TMachine.Create(...)machine.Assign(AnotherMachine) 等),而在另一个代码中您使用 TAutomobile,它们永远不会混淆 那么你就没事了,你可以忽略这些警告或使用reintroduce“静音”它们。

    但是继承通常还有另一个方面:保持统一的接口,或者有时称为:“契约”。将接口与实现分离。

    例如,表单能够释放所有属于它的对象,无论这些对象是什么,这是因为 Destroy 方法被覆盖。 Form 不关心你的实现,但它知道:释放对象只需调用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) 方法并覆盖它,否则它永远不会被调用(而是调用 inherited)。

    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 writer 知道这可能会发生并实现了AssignTo 方法,该方法能够将其转换为位图。所以TBitmap 作为最后一根稻草调用TPersistent.Assign 方法,然后又调用TPngImage.AssignTo,这就像一个魅力。

    您的程序是否需要继承这一方面取决于您。如果再次出现大量重复代码(一个处理机器,另一个处理汽车)或有很多条件,则说明有问题,一些好的多态性可能会有所帮助。

    【讨论】:

    • 其实TPngImageTBitmap继承自TGraphic。您的示例表明 TPngImage 继承自 TBitmap,这是错误的。
    • @JerryDodge 我不是那个意思。只是我想到的第一个例子 TPersistent.Assign 应该如何工作。 TPngImage 和 TBitmap 可能都是 TPersistent 的后代,无论如何都可以工作(如果 TPngImage 作者认为分配给 TBitmap 是有用的东西并实现这种情况)。
    • 确实,我敢肯定你不是那个意思。只是指出它,因为它可能会误导新人。但我认为这是一个更好的例子来说明TGraphic 如何是基础,而继承的类TBitmapTPngImage(以及其他)以不同的方式实现标准结构。 TGraphic 是一个抽象类,可以通过继承类进一步实现,这些继承类覆盖(并重新引入)其祖先的成员。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-30
    相关资源
    最近更新 更多