在 Ada 中从基础 class 调用重写的方法

Calling an overridden method from the base class in Ada

我想知道如何从 ADA 中的 parent class 调用覆盖的方法。让我们考虑以下示例。 Parent class 有一些方法被 Child class 覆盖。 Parent class(即Prints)中有一个方法调用了它的一些重写方法。但是重写的方法没有被调用!这是示例:

--- parent ---

package Parents is 
    type Parent is tagged null record;

    procedure Prints(Self: in out Parent); 

    -- these will be overridden  
    procedure Print1(Self: in out Parent) is null;
    procedure Print2(Self: in out Parent) is null;        
end Parents;
...
package body Parents is        
    procedure Prints(Self: in out Parent) is
    begin
        Put_Line("Parents.Prints: calling prints...");
        Self.Print1;
        Self.Print2;
    end;        
end Parents;

--- child ---

With Parents;
package Childs is 
    type Child is new Parents.Parent with null record; 

    overriding procedure Print1(Self: in out Child);
    overriding procedure Print2(Self: in out Child);    
end Childs;
...
package body Childs is         
    procedure Print1(Self: in out Child) is
    begin
        Put_Line("Child.Print1 is printing...");
    end;

    procedure Print2(Self: in out Child) is
    begin
        Put_Line("Child.Print2 is printing...");
    end;        
end Childs;

---主要---

procedure Main is  
    anyprint : access Parents.Parent'Class;
begin           
    anyprint := new Childs.Child;
    anyprint.Prints;
end Main;

问题

我希望看到的是从 Child 发送到 Print1Print2 的调用。但是重写的方法没有被调用!有 C++ 背景,这种类型的多态调用对我来说很有意义,但我不知道 Ada 如何对待它们?

来自Prints的调用Self.Print1;是否错误?

在 Ada 中,仅当对象属于 class-wide 类型时才会进行分派。相关的手册部分是 ARM 3.9.2.

Parents.Prints中,控制操作数Self的类型是Parent,并且是“静态标记”,所以没有调度。

一种方法是使用“重新调度”,如下所示:

procedure Prints(Self: in out Parent) is
begin
   Put_Line("Parents.Prints: calling prints...");
   Parent'Class (Self).Print1;
   Parent'Class (Self).Print2;
end;

其中视图转换 Parent'Class (Self) 表示调用 .Print1 的对象是 "dynamically tagged" 并且调用调度。

如您所见,Prints 可以在派生类型中被覆盖。这并不总是(或什至通常?)您想要的。如果不是,将其参数更改为 class-wide:

是合适的
procedure Prints(Self: in out Parent'Class);

(当然还有体内!)然后一切如您所愿。

[旁注:我现在了解到 object.operation 表示法适用于 class 范围的对象!]

思考这个问题的一种方法是从低层次思考它,即编译器生成什么代码。

编译Prints过程时,看到语句Self.Print1,编译器生成的代码是非分派调用。这意味着编译器计算出 Print1 方法的地址(或外部符号),并生成对它的调用。这不是间接调用,而是对固定地址的调用,即Parents中出现的Print1。它不分派的原因是 Self 的类型不是类范围类型(​​它只是 Parent,而不是 Parent'Class)。

当您声明Child 类型时,它将继承Prints 过程。也就是说,有一个隐式声明的过程如下所示:

procedure Prints (Self : in out Child);  -- inherited procedure

但是如果您不重写它,编译器不会为这个隐式过程生成新代码。所以当 Prints 被调用时,即使它是用 Child 参数调用的,代码也会和 Parent 参数一样。如前一段所述,代码对 Print1Print2 进行固定调用,而不是调度(间接)调用,它们仍然是 Parent 中声明的那些,因为代码是编译Parent时生成的。

返回 Prints 中的通话:

Self.Print1;

如果 Self 的类型是 Parent'Class,或者如果您使用视图转换将其转换为 Parent'Class,如 Simon 的回答 (Parent'Class(Self)),那么调用将是 dispatching,这意味着它基本上是一个间接调用。该代码将在 运行 时间计算出正确过程的地址,并间接调用它。

Ada 和 C++ 的不同之处在于,Ada 编译器使用它正在操作的对象的 类型 来确定是进行调度(间接)还是非调度(称呼)。如果类型是类范围的,则它是调度的,否则它是固定的。但是,C++ 使用方法的 属性 而不是类型的 属性 来决定进行哪种调用;如果该方法被标记为 virtual,则对它的调用正在调度,如果没有,则它们是固定的。 (至少我认为是这样;我不是真正的 C++ 专家。)

顺便说一句,即使您不使用 Object.Operation 符号,这同样适用。如果您说的不是 Self.Print1,而是

Print1 (Self);

那将是一个非调度呼叫;但是

Print1 (Parent'Class (Self));

是调度呼叫。