【问题标题】:Delphi: Understanding constructorsDelphi:理解构造函数
【发布时间】:2011-04-22 00:30:12
【问题描述】:

我希望了解

  • 虚拟
  • 覆盖
  • 过载
  • 重新介绍

应用于对象构造函数时。每次我随机添加关键字直到编译器关闭 - 并且(在使用 Delphi 开发 12 年后)我宁愿知道我在做什么,而不是随机尝试。

给定一组假设的对象:

TComputer = class(TObject)
public
    constructor Create(Cup: Integer); virtual;
end;

TCellPhone = class(TComputer)
public
    constructor Create(Cup: Integer; Teapot: string); virtual;
end;

TiPhone = class(TCellPhone)
public
    constructor Create(Cup: Integer); override;
    constructor Create(Cup: Integer; Teapot: string); override;
end;

我希望他们的行为方式从声明中可能很明显,但是:

  • TComputer 有简单的构造函数,后代可以覆盖它
  • TCellPhone 有一个备用构造函数,后代可以覆盖它
  • TiPhone 覆盖两个构造函数,调用每个构造函数的继承版本

现在该代码无法编译。我想了解为什么它不起作用。我也想了解重写构造函数的正确方法。或者也许你永远无法覆盖构造函数?或者也许重写构造函数是完全可以接受的?也许你永远不应该有多个构造函数,也许拥有多个构造函数是完全可以接受的。

我想了解为什么。修复它就很明显了。

另见

编辑:我也希望按virtualoverrideoverloadreintroduce 的顺序进行推理。因为在尝试所有关键字组合时,组合的数量会爆炸式增长:

  • 虚拟;过载;
  • 虚拟;覆盖;
  • 覆盖;过载;
  • 覆盖;虚拟的;
  • 虚拟;覆盖;过载;
  • 虚拟;超载;覆盖;
  • 过载;虚拟的;覆盖;
  • 覆盖;虚拟的;过载;
  • 覆盖;超载;虚拟的;
  • 过载;覆盖;虚拟的;

编辑 2: 我想我们应该从“给定的对象层次结构是否可能?”开始,如果没有,为什么不呢?例如,使用祖先的构造函数是根本不正确的吗?

TComputer = class(TObject)
public
    constructor Create(Cup: Integer); virtual;
end;

TCellPhone = class(TComputer)
public
    constructor Create(Cup: Integer; Teapot: string); virtual;
end;

我希望TCellPhone 现在有两个构造函数。但我无法在 Delphi 中找到关键字组合,使其认为这是一件有效的事情。我认为我可以在TCellPhone 中有两个构造函数,我是否从根本上错了?


注意:此行以下的所有内容并非严格要求回答 问题 - 但它确实有助于解释 我的想法。也许你可以看到, 根据我的思维过程,什么 我缺少的基本部分 让一切都清楚。

现在这些声明无法编译:

//Method Create hides virtual method of base type TComputer:
TCellPhone = class(TComputer)
   constructor Create(Cup: Integer; Teapot: string);  virtual;

//Method Create hides virtual method of base type TCellPhone:
TiPhone = class(TCellPhone)
public
   constructor Create(Cup: Integer); override;
   constructor Create(Cup: Integer; Teapot: string); overload;  <--------
end;

所以首先我会尝试修复TCellPhone。我将从随机添加 overload 关键字开始(我知道我不想要 reintroduce 因为那会隐藏另一个我不想要的构造函数):

TCellPhone = class(TComputer)
public
   constructor Create(Cup: Integer; Teapot: string); virtual; overload;
end;

但这失败了:Field definition not allowed after methods or properties

我从经验中知道,即使我在方法或属性之后没有字段,如果我颠倒 virtualoverload 关键字的顺序:Delphi 会闭嘴:

TCellPhone = class(TComputer)
public
   constructor Create(Cup: Integer; Teapot: string); overload; virtual; 
end;

但我仍然得到错误:

方法 'Create' 隐藏基本类型 'TComputer' 的虚拟方法

所以我尝试删除这两个关键字:

TCellPhone = class(TComputer)
public
   constructor Create(Cup: Integer; Teapot: string);
end;

但我仍然得到错误:

方法 'Create' 隐藏基本类型 'TComputer' 的虚拟方法

所以我辞职现在尝试reintroduce

TCellPhone = class(TComputer)
public
   constructor Create(Cup: Integer; Teapot: string); reintroduce;
end;

现在 TCellPhone 可以编译,但它让 TiPhone 的情况变得更糟:

TiPhone = class(TCellPhone)
public
   constructor Create(Cup: Integer); override; <-----cannot override a static method
   constructor Create(Cup: Integer; Teapot: string); override; <-----cannot override a static method
end;

两者都抱怨我无法覆盖它们,所以我删除了 override 关键字:

TiPhone = class(TCellPhone)
public
   constructor Create(Cup: Integer);
   constructor Create(Cup: Integer; Teapot: string);
end;

但现在第二次创建说它必须标记为重载,我这样做了(实际上我会将两者都标记为重载,因为我知道如果我不这样做会发生什么):

TiPhone = class(TCellPhone)
public
   constructor Create(Cup: Integer); overload;
   constructor Create(Cup: Integer; Teapot: string); overload;
end;

interface 部分中的一切都很好。不幸的是,我的实现不起作用。我的TiPhone单参数构造函数不能调用继承的构造函数:

constructor TiPhone.Create(Cup: Integer);
begin
    inherited Create(Cup); <---- Not enough actual parameters
end;

【问题讨论】:

  • 来自 Delphi 9 帮助:方法声明可以包含不与其他函数或过程一起使用的特殊指令。指令应仅出现在类声明中,而不应出现在定义声明中,并且应始终按以下顺序列出:reintroduce; overload; 绑定; 调用约定; abstract; 警告,其中绑定为virtualdynamicoverride;调用约定为registerpascalcdeclstdcallsafecall;警告是platformdeprecatedlibrary
  • Delphi 5 仍然带有出色的印刷语言指南,详细解释了这些声明。
  • 试试 TCellPhone.Create(Cup);而不是继承的 Create(Cup);

标签: delphi constructor delphi-5 constructor-chaining


【解决方案1】:

请注意,我没有 Delphi 5,所以我的答案基于最新版本 Delphi XE。我认为这不会真正产生任何影响,但如果确实如此,你已经被警告过。 :)

这主要基于http://docwiki.embarcadero.com/RADStudio/en/Methods,这是有关方法如何工作的当前文档。您的 Delphi 5 帮助文件可能也有类似的内容。

首先,虚拟构造函数在这里可能没有多大意义。在某些情况下您确实想要这个,但这可能不是一个。查看http://docwiki.embarcadero.com/RADStudio/en/Class_References 了解您确实需要虚拟构造函数的情况 - 如果您在编码时始终知道对象的类型,但您不知道。

您在单参数构造函数中遇到的问题是您的父类本身没有单参数构造函数 - 继承的构造函数没有公开。您不能使用inherited 在层次结构中上升多个级别,您只能调用您的直接父级。您将需要使用一些默认值调用 2 参数构造函数,或者也将 1 参数构造函数添加到 TCellPhone。

一般来说,四个关键词的含义如下:

  • virtual - 将此标记为您希望运行时调度的函数(允许多态行为)。这仅适用于初始定义,不适用于子类中的覆盖。
  • override - 为虚拟方法提供新的实现。
  • overload - 将一个函数标记为与另一个函数同名,但参数列表不同。
  • reintroduce - 告诉编译器你实际上打算隐藏一个虚方法,而不是仅仅忘记提供override

文档中详细说明了所需的订购方式:

方法声明可以包括 未使用的特殊指令 与其他功能或程序。 指令应该出现在类中 仅声明,不在定义中 声明,并且应该始终是 按以下顺序列出:

重新介绍;超载;捆绑; 调用约定;抽象的;警告

绑定是虚拟的、动态的或 覆盖;调用约定是 register、pascal、cdecl、stdcall 或 安全呼叫;警告是平台, 已弃用或库。

【讨论】:

  • 为什么更简单的例子,只使用前两个类,仍然失败?编译器抱怨Create(int, string) 隐藏了基础Create(int)。它有不同的签名,它是如何隐藏的?
  • @Ian,他们有相同的名字。如果你想要两个具有相同名称但不同签名的东西,那么你就是 overloading,并且 both 声明需要使用 overload 指令。
  • @Ian:另外,如果你根本不使用virtual,编译器不应该发出隐藏任何东西的警告——构造函数直接与它们的类相关联。
  • @Rob Kennedy 你能举例说明如何添加overload 指令吗? (就像我说的,我已经尝试了关键字的每一个随机组合——我不能让它不抱怨)。另一个答案的形式将是完美的 - 允许可读的代码。
  • 构造函数不是虚拟的问题是一个构造函数调用同级构造函数不会在后代中获得覆盖的实现。
【解决方案2】:

我看到您的原始声明集不应该编译干净的两个原因:

  1. TCellPhone 中应该有一个警告,它的构造函数隐藏基类的方法。这是因为基类方法是 virtual,编译器担心您引入了一个具有相同名称的 new 方法而没有覆盖基类方法。签名不同并不重要。如果您的意图确实是隐藏基类的方法,那么您需要在后代声明中使用reintroduce,正如您的一个盲目猜测所示。该指令的唯一目的是平息警告;它对运行时行为没有影响。

    忽略TIPhone 稍后会发生什么,下面的TCellPhone 声明就是您想要的。它隐藏了祖先方法,但您也希望它是虚拟的。它不会继承祖先方法的虚拟性,因为它们是两个完全独立的方法,只是碰巧具有相同的名称。因此,您还需要在新声明中使用virtual

    TCellPhone = class(TComputer)
    public
      constructor Create(Cup: Integer; Teapot: string); reintroduce; virtual;
    end;
    

    基类构造函数TComputer.Create也隐藏了祖先TObject.Create的方法,但由于TObject中的方法不是虚拟的,编译器不会警告它。隐藏非虚拟方法一直在发生,而且通常不起眼。

  2. 您应该在TIPhone 中收到一个错误,因为不再有任何单参数构造函数可以覆盖。你把它藏在TCellPhone。由于您想要有两个构造函数,reintroduce 显然 不是早先使用的正确选择。您不想隐藏基类构造函数;你想用另一个构造函数来扩充它。

    由于您希望两个构造函数具有相同的名称,因此您需要使用overload 指令。该指令需要用于所有原始声明——第一次引入每个不同的签名后代中的后续声明。我认为 all 声明(甚至是基类)都需要它,这样做并没有什么坏处,但我想这不是必需的。因此,您的声明应如下所示:

    TComputer = class(TObject)
    public
      constructor Create(Cup: Integer);
        overload; // Allow descendants to add more constructors named Create.
        virtual;  // Allow descendants to re-implement this constructor.
    end;
    
    TCellPhone = class(TComputer)
    public
      constructor Create(Cup: Integer; Teapot: string);
        overload; // Add another method named Create.
        virtual;  // Allow descendants to re-implement this constructor.
    end;
    
    TiPhone = class(TCellPhone)
    public
      constructor Create(Cup: Integer);
        override; // Re-implement the ancestor's Create(Integer).
      constructor Create(Cup: Integer; Teapot: string);
        override; // Re-implement the ancestor's Create(Integer, string).
    end;
    

Modern documentation 告诉一切应该按什么顺序排列:

重新介绍重载; 绑定; 调用约定; 抽象警告

其中绑定虚拟动态覆盖调用约定registerpascalcdeclstdcall安全呼叫; warningplatformdeprecatedlibrary

这是六个不同的类别,但根据我的经验,很少有任何声明超过三个。 (例如,需要指定调用约定的函数可能不是方法,因此它们不能是虚拟的。)我不记得顺序;直到今天我还没有看到它记录在案。相反,我认为记住每个指令的用途会更有帮助。当您记住不同任务所需的指令时,您最终只会得到两个或三个,然后进行实验以获得有效订单非常简单。编译器可能接受多个顺序,但不要担心——顺序在确定含义时并不重要。编译器接受的任何顺序都与其他任何顺序具有相同的含义(调用约定除外;如果您提到多个其中一个,则只有最后一个才重要,所以不要这样做)。

所以,你只需要记住每个指令的目的,并考虑哪些放在一起没有任何意义。例如,您不能同时使用reintroduceoverride,因为它们的含义相反。而且你不能同时使用virtualoverride,因为一个暗示另一个。

如果您有很多指令堆积,您可以在制定所需的其余指令时始终将overload 排除在外。给你的方法起不同的名字,找出他们自己需要的other指令,然后在你给它们所有相同的名字的同时加上overload

【讨论】:

    【解决方案3】:

    这是所需定义的有效实现:

    program OnConstructors;
    
    {$APPTYPE CONSOLE}
    
    uses
      SysUtils;
    
    type
    
    TComputer = class(TObject)
    public
        constructor Create(Cup: Integer); virtual;
    end;
    
    TCellPhone = class(TComputer)
    public
        constructor Create(Cup: Integer; Teapot: string); reintroduce; overload; virtual;
    end;
    
    TiPhone = class(TCellPhone)
    public
        constructor Create(Cup: Integer); overload; override;
        constructor Create(Cup: Integer; Teapot: string); override;
    end;
    
    { TComputer }
    
    constructor TComputer.Create(Cup: Integer);
    begin
      Writeln('Computer: cup = ', Cup);
    end;
    
    { TCellPhone }
    
    constructor TCellPhone.Create(Cup: Integer; Teapot: string);
    begin
      inherited Create(Cup);
      Writeln('Cellphone: teapot = ', Teapot);
    end;
    
    { TiPhone }
    
    constructor TiPhone.Create(Cup: Integer);
    begin
      inherited Create(Cup);
      Writeln('iPhone: cup = ', Cup);
    end;
    
    constructor TiPhone.Create(Cup: Integer; Teapot: string);
    begin
      inherited;
      Writeln('iPhone: teapot = ', Teapot);
    end;
    
    var
      C: TComputer;
    
    begin
    
      C := TComputer.Create(1);
      Writeln; FreeAndNil(C);
    
      C := TCellPhone.Create(2);
      Writeln; FreeAndNil(C);
      C := TCellPhone.Create(3, 'kettle');
      Writeln; FreeAndNil(C);
    
      C := TiPhone.Create(4);
      Writeln; FreeAndNil(C);
      C := TiPhone.Create(5, 'iPot');
    
      Readln; FreeAndNil(C);
    
      end.
    

    结果:

    Computer: cup = 1
    
    Computer: cup = 2
    
    Computer: cup = 3
    Cellphone: teapot = kettle
    
    Computer: cup = 4
    iPhone: cup = 4
    
    Computer: cup = 5
    Cellphone: teapot = iPot
    iPhone: teapot = iPot
    

    第一部分按照thisTiPhone 两个构造函数的定义如下:

    • 第一个构造函数重载一个继承的两个构造函数并覆盖其兄弟。为此,请使用 overload; override 重载 TCellPhone 一个,同时覆盖另一个构造函数。
    • 完成后,第二个构造函数需要一个简单的override 来覆盖其兄弟。

    【讨论】:

      【解决方案4】:

      在两者上都使用重载,这是我的做法,而且有效。

      constructor Create; Overload;

      constructor Values; Overload;

      记住不要为两个不同的构造函数使用相同的名字

      【讨论】:

        猜你喜欢
        • 2017-01-21
        • 1970-01-01
        • 2014-10-07
        • 1970-01-01
        • 1970-01-01
        • 2017-04-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多