跳转到内容

Ada 编程/面向对象

来自 Wikibooks,开放世界中的开放书籍

Ada. Time-tested, safe and secure.
Ada。经久耐用、安全可靠。

Ada 中的面向对象

[编辑 | 编辑源代码]

面向对象编程是指以“对象”为单位构建软件。一个“对象”包含数据并具有行为。数据通常由常量和变量组成,如本书其他部分所述,但也可以在程序之外,例如磁盘或网络上。行为由对数据进行操作的子程序组成。与过程式编程相比,面向对象编程的独特之处不在于单个特性,而是几个特性的组合。

  • 封装,即能够将对象的实现与其接口分离;这反过来又将对象的“客户端”(只能以某些预定义方式使用对象)与对象的内部(对外部客户端一无所知)分离。
  • 继承,一种类型的对象能够继承另一种类型的对象的數據和行为(子程序),而无需打破封装;
  • 类型扩展,对象能够在继承的对象的基础上添加新的数据组件和新的子程序,并用自己的版本替换一些继承的子程序;这称为覆盖
  • 多态性,"客户端"能够在不知道对象的确切类型的情况下使用对象的服務,即以抽象的方式。实际上,在运行时,实际对象在每次调用时可能具有不同的类型。

任何语言都可以进行面向对象编程,即使是汇编语言。然而,如果没有语言支持,类型扩展和多态性很难实现。

在 Ada 中,每个概念都有一个匹配的构造;这就是 Ada 直接支持面向对象编程的原因。

  • 包提供封装;
  • 派生类型提供继承;
  • 记录扩展(如下所述)提供类型扩展;
  • 类范围类型(如下所述)提供多态性。

Ada 从第一个版本(1980 年的 MIL-STD-1815)开始就拥有封装和派生类型,这导致一些人以非常狭义的意义将该语言归类为“面向对象”的。记录扩展和类范围类型是在 Ada 95 中添加的。Ada 2005 进一步增加了接口。本章的其余部分将涵盖这些方面。

最简单的对象:单例

[编辑 | 编辑源代码]
package Directory is
  function Present (Name_Pattern: String) return Boolean;
  generic
     with procedure Visit (Full_Name, Phone_Number, Address: String;
                           Stop: out Boolean);
  procedure Iterate (Name_Pattern: String);
end Directory;

目录是一个对象,包含数据(电话号码和地址,可能存储在外部文件或数据库中)和行为(它可以查找条目并遍历与 Name_Pattern 匹配的所有条目,对每个条目调用 Visit)。

一个简单的包提供封装(目录的内部机制被隐藏),一对子程序提供行为。

这种模式适用于只允许存在一个特定类型对象的场景;因此,不需要类型扩展或多态性。

基本操作

[编辑 | 编辑源代码]

在 Ada 中,方法通常被称为带标记类型的基本子程序或等效术语带标记类型的基本操作。类型的基本操作是始终在使用类型的地方可用的操作。对于面向对象编程中使用的带标记类型,它们也可以被派生类型继承和覆盖,并且可以动态分派。

类型的基本操作需要在与类型相同的包中声明(不能在嵌套包或子包中)。对于带标记类型,在类型冻结点之前,还需要声明新的基本操作和对继承的基本操作的覆盖。在冻结点之后声明的任何子程序都不会被视为基本程序,因此不能被继承,也不会进行动态分派。冻结点将在下面更详细地讨论,但将所有基本操作声明在初始类型声明之后,这个简单的做法将确保这些子程序确实被识别为基本程序。

类型 T 的基本操作至少需要有一个类型为Taccess T的参数。虽然大多数面向对象语言会自动提供thisself指针,但 Ada 要求显式声明一个形式参数来接收当前对象。该参数通常是参数列表中的第一个参数,它允许object.subprogram调用语法(从 Ada 2005 开始可用),但它可以位于任何参数位置。带标记类型始终按引用传递;参数传递方式与参数模式inout无关,这些模式描述了数据流。对于Taccess T,参数传递方式相同。

对于带标记类型,参数列表中不能使用其他直接可分派类型,因为 Ada 不提供多重分派。以下示例是非法的。

package P is
   type A is tagged private;
   type B is tagged private;
   procedure Proc (This: B; That: A); -- illegal: can't dispatch on both A and B
end P;

当需要传递额外的可分派对象时,参数列表应使用它们的类范围类型T'Class来声明它们。例如

package P is
   type A is tagged private;
   type B is tagged private;
   procedure Proc (This: B; That: A'Class); -- dispatching only on B
end P;

但是,这并不限制相同带标记类型参数的数量。例如,以下定义是合法的。

package P is
   type A is tagged private;
   procedure Proc (This, That: A); -- dispatching only on A
end P;

带标记类型的基本操作是分派操作。对这种基本操作的调用实际上是分派调用还是静态绑定,取决于上下文(见下文)。请注意,在分派调用中,最后一个示例的两个实际参数必须具有相同的标记(即相同的类型);如果标记检查失败,将调用 Constraint_Error。

派生类型

[编辑 | 编辑源代码]

类型派生一直是 Ada 的核心部分。

package P is
  type T is private;
  function Create (Data: Boolean) return T;  -- primitive
  procedure Work (Object : in out T);        -- primitive
  procedure Work (Pointer: access T);        -- primitive
  type Acc_T is access T;
  procedure Proc (Pointer: Acc_T);           -- not primitive
private
  type T is record
    Data: Boolean;
  end record;
end P;

上面的示例创建了一个包含数据(这里只是一个布尔值,但可以是任何东西)和行为的类型 T,行为包括一些子程序。它还通过将类型 T 的详细信息放在包的 private 部分来演示封装。

T 的基本操作是函数 Create、重载过程 Work 和预定义的“=”运算符;Proc 不是基本程序,因为它使用 T 的访问类型作为参数——不要将此与访问参数混淆,如第二个过程 Work 中使用的那样。从 T 派生时,会继承基本操作。

with P;
package Q is
  type Derived is new P.T;
end Q;

类型 Q.Derived 具有与 P.T 相同的数据以及相同行为;它继承了数据和子程序。因此,可以编写以下代码

with Q;
procedure Main is
  Object: Q.Derived := Q.Create (Data => False);
begin
  Q.Work (Object);
end Main;

继承的操作可以被覆盖,也可以添加新的操作,但规则(Ada 83)不幸地与带标记类型(Ada 95)的规则有所不同。

诚然,编写此代码的原因可能看起来很模糊。这种代码的目的是拥有类型 P.T 和 Q.Derived 的对象,它们是不兼容的

Ob1: P.T;
Ob2: Q.Derived;
Ob1 := Ob2;              -- illegal
Ob1 := P.T (Ob2);        -- but convertible
Ob2 := Q.Derived (Ob1);  -- in both directions

这种特性并不经常使用(例如,用于声明反映物理维度的类型),但我在这里介绍它,以便引入下一步:类型扩展。

类型扩展

[编辑 | 编辑源代码]

类型扩展是 Ada 95 的一个修正。

带标签类型支持动态多态和类型扩展。带标签类型包含一个隐藏的标签,在运行时识别类型。除了标签之外,带标签记录就像任何其他记录一样,因此它可以包含任意数据。

package Person is
   type Object is tagged
     record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
     end record;
   procedure Put (O : Object);
end Person;

如您所见,Person.Object 在某种意义上是一个对象,因为它具有数据和行为(过程 Put)。但是,此对象不会隐藏其数据;任何具有 with Person 子句的程序单元都可以直接读取和写入 Person.Object 中的数据。这破坏了封装,也说明了 Ada 完全将封装类型的概念分开。以下是一个封装了其数据的 Person.Object 版本

package Person is
   type Object is tagged private;
   procedure Put (O : Object);
private
   type Object is tagged
     record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
     end record;
end Person;

因为类型 Person.Object 带有标签,所以可以创建记录扩展,它是一个具有额外数据的派生类型。

with Person;
package Programmer is
   type Object is new Person.Object with private;
private
   type Object is new Person.Object with
     record
        Skilled_In : Language_List;
     end record;
end Programmer;

类型 Programmer.Object 继承了 Person.Object 的数据和行为,即类型的基本操作;因此可以编写

with Programmer;
procedure Main is
   Me : Programmer.Object;
begin
   Programmer.Put (Me);
   Me.Put; -- equivalent to the above, Ada 2005 only
end Main;

因此,类型 Programmer.Object 作为 Person.Object 的记录扩展的声明,隐式声明了一个 procedure Put,它适用于 Programmer.Object

与无标签类型一样,Person 和 Programmer 类型的对象是可转换的。但是,在无标签对象可以双向转换的情况下,带标签类型的转换仅适用于根方向。(远离根的转换将必须凭空添加组件。)这种转换称为视图转换,因为组件不会丢失,它们只是变得不可见。

如果您要离开根,则必须使用扩展聚合。

现在我们已经引入了带标签类型、记录扩展和基本操作,就能够理解覆盖了。在上面的示例中,我们引入了一个名为 Person.Object 的类型,它有一个名为 Put 的基本操作。以下是包的主体

with Ada.Text_IO;
package body Person is
   procedure Put (O : Object) is
   begin
      Ada.Text_IO.Put (O.Name);
      Ada.Text_IO.Put (" is a ");
      Ada.Text_IO.Put_Line (Gender_Type'Image (O.Gender));
   end Put;
end Person;

如您所见,此简单操作会将记录类型的两个数据组件都打印到标准输出。现在,请记住记录扩展 Programmer.Object 具有一个额外的数据成员。如果我们编写

with Programmer;
procedure Main is
   Me : Programmer.Object;
begin
   Programmer.Put (Me);
   Me.Put; -- equivalent to the above, Ada 2005 only
end Main;

那么程序将调用继承的基本操作 Put,它将打印姓名和性别但不会打印额外数据。为了提供此额外行为,我们必须覆盖继承的过程 Put,如下所示

with Person;
package Programmer is
   type Object is new Person.Object with private;
   overriding -- Optional keyword, new in Ada 2005
   procedure Put (O : Object);
private
   type Object is new Person.Object with
     record
        Skilled_In : Language_List;
     end record;
end Programmer;
package body Programmer is
   procedure Put (O : Object) is
   begin
      Person.Put (Person.Object (O)); -- view conversion to the ancestor type
      Put (O.Skilled_In); -- presumably declared in the same package as Language_List
   end Put;
end Programmer;

Programmer.Put 覆盖Person.Put;换句话说,它完全替换了它。由于目的是扩展行为而不是替换行为,Programmer.PutPerson.Put 作为其行为的一部分调用。它通过将参数从类型 Programmer.Object 转换为其祖先类型 Person.Object 来实现这一点。此结构是一个视图转换;与普通类型转换相反,它不会创建新对象,不会产生任何运行时成本(实际上,如果这种视图转换的操作数实际上是一个变量,则结果可以在需要输出参数时使用(例如过程调用)。当然,覆盖操作是否调用其祖先是可以选择的;在某些情况下,目的是确实要替换,而不是扩展继承的行为。

(请注意,对于无标签类型,也可以覆盖继承的操作。之所以在这里讨论它,是因为无标签类型的派生很少见。)

多态、类范围编程和动态分派

[编辑 | 编辑源代码]

面向对象技术的全部力量是通过多态、类范围编程和动态分派实现的,它们是同一概念的不同说法。为了解释此概念,让我们扩展前面部分的示例,在该示例中,我们声明了一个名为 Person.Object 的基本带标签类型,它有一个名为 Put 的基本操作,以及一个名为 Programmer.Object 的记录扩展,它具有额外数据和一个覆盖的基本操作 Put

现在,让我们想象一个包含多个人的集合。在该集合中,有些人是程序员。我们想要遍历集合,并对每个人调用 Put。当被考虑的人是程序员时,我们想要调用 Programmer.Put;当该人不是程序员时,我们想要调用 Person.Put。从本质上讲,这就是多态、类范围编程和动态分派。

有了 Ada 的强类型,普通调用无法进行动态分派;对声明类型上的操作的调用必须始终静态绑定到该特定类型定义的操作。动态分派(在 Ada 术语中被称为分派)是通过单独的类范围类型提供的,这些类型是多态的。每个带标签类型(例如 Person.Object)都有一个相应的类型类,它是由 Person.Object 本身以及扩展 Person.Object 的所有类型组成的类型集。在我们的示例中,此类包含两种类型

  • Person.Object
  • Programmer.Object

Ada 95 定义了 Person.Object'Class 属性来表示相应的类范围类型。换句话说

declare
   Someone : Person.Object'Class := ...; -- to be expanded later
begin
   Someone.Put; -- dynamic dispatching
end;

Someone 的声明表示一个可能是任何类型的对象,Person.ObjectProgrammer.Object。因此,对基本操作 Put 的调用将动态分派到 Person.PutProgrammer.Put

唯一的问题是,由于我们不知道 Someone 是否是程序员,所以我们也不知道 Someone 拥有多少个数据组件,因此我们也不知道 Someone 在内存中占用了多少字节。出于这个原因,类范围类型 Person.Object'Class不定的。在不指定任何约束的情况下,不可能声明此类型的对象。但是,可以

  • 声明一个具有初始值的类范围对象(如上)。然后,该对象会受到其初始值的约束。
  • 声明对该对象的访问值(因为访问值具有已知大小);
  • 将类范围类型的对象作为参数传递给子程序
  • 将特定类型的对象(特别是函数调用的结果)分配给类范围类型的变量。

有了这些知识,我们现在可以构建一个包含多个人的多态集合;在这个例子中,我们将简单地创建一个包含对人的访问值的数组

with Person;
procedure Main is
   type Person_Access is access Person.Object'Class;
   type Array_Of_Persons  is array (Positive range <>) of Person_Access;

   function Read_From_Disk return Array_Of_Persons is separate;

   Everyone : constant Array_Of_Persons := Read_From_Disk;
begin -- Main
   for K in Everyone'Range loop
      Everyone (K).all.Put; -- dereference followed by dynamic dispatching
   end loop;
end Main;

上面的过程实现了我们想要的目标:它遍历 Persons 数组,并调用适合每个人的过程 Put

高级主题:动态分派的工作原理

[编辑 | 编辑源代码]

您不需要了解动态分派的工作原理就能有效地使用它,但如果您好奇,以下是一个解释。

内存中每个对象的第一个组件是标签;这就是为什么对象是带标签类型而不是普通记录的原因。标签实际上是对表的访问值;每个特定类型都有一个表。表包含对该类型每个基本操作的访问值。在我们的示例中,由于存在两种类型 Person.ObjectProgrammer.Object,因此存在两个表,每个表包含一个访问值。Person.Object 的表包含对 Person.Put 的访问值,而 Programmer.Object 的表包含对 Programmer.Put 的访问值。当您编译程序时,编译器会构建这两个表并将它们放在程序可执行代码中。

程序每次创建特定类型的新对象时,都会自动将其标签设置为指向相应的表。

程序每次执行基本操作的分派调用时,编译器都会插入以下对象代码

  • 取消对标签的引用以查找当前对象特定类型的基本操作表
  • 取消对基本操作访问值的引用
  • 调用基本操作。

相反,当程序执行参数为对祖先类型的视图转换的调用时,编译器会在编译时而不是运行时执行这两个取消引用操作:此类调用是静态绑定的;编译器会发出直接调用视图转换中指定的祖先类型基本操作的代码。

重新分派

[编辑 | 编辑源代码]

分派由对象的(隐藏)标签控制。那么,当基本操作 Op1 对同一个对象调用另一个基本操作 Op2 时会发生什么呢?

 type Root is tagged private;
 procedure Op1 (This: Root);
 procedure Op2 (This: Root);

 type Derived is new Root with private;
 -- Derived inherits Op1
 overriding procedure Op2 (This: Derived);

 procedure Op1 (This: Root) is
 begin
   ...
   Op2 (This);               -- not redispatching
   Op2 (Root'Class (This));  -- redispatching
   This.Op2;                 -- not redispatching (new syntax since Ada 2005)
   (Root'Class (This)).Op2;  -- redispatching (new syntax since Ada 2005)
   ...
 end Op1;

 D: Derived;
 C: Root'Class := D;

 Op1 (D);  -- statically bound call
 Op1 (C);  -- dispatching call
 D.Op1;    -- statically bound call (new syntax since Ada 2005)
 C.Op1;    -- dispatching call (new syntax since Ada 2005)

在此片段中,Op1 没有被覆盖,而 Op2 被覆盖了。Op1 的主体调用 Op2,那么如果 Op1 被用于 Derived 类型的对象调用,将会调用哪个 Op2 呢?

分派的规则仍然适用。对 Op2 的调用将在使用类范围类型的对象调用时进行分派。

操作的正式参数列表指定了This的类型为一个特定类型,而不是类范围类型。事实上,该参数必须是特定类型,以便在该类型的对象上分派操作,并允许操作的代码访问与该类型相关联的任何附加数据项。如果您希望重新分派,则必须通过将特定类型的参数再次转换为类范围类型来显式声明。 (记住:视图转换永远不会丢失组件,它们只是隐藏它们。转换为类范围类型可以再次取消隐藏它们。)第一个调用Op1 (D)(静态绑定,即不分派)执行继承的Op1 - 并在Op1中,对Op2的第一次调用也静态绑定(没有重新分派),因为参数This是到特定类型Root的视图转换。但是,第二次调用是分派,因为参数This被转换为类范围类型。该调用分派到重写Op2

由于传统的This.Op2调用不是分派,因此即使对象本身是Derived类型,并且Op2操作被重写,该调用也会调用Root.Op2这与其他面向对象语言的行为有很大不同。在其他面向对象语言中,方法要么是分派的,要么不是。在 Ada 中,操作要么可用于分派,要么不可用。对于给定调用是否实际使用分派取决于在该调用点指定对象类型的方式。对于习惯于其他面向对象语言的程序员来说,来自可分派操作对同一对象上的其他操作的调用默认情况下(动态)分派可能会让人感到意外。

如果所有操作都已被重写,则不重新分派默认值不会成为问题,因为它们都将对预期的对象类型进行操作。但是,在为将来可能被另一种类型扩展的类型编写代码时,它会产生影响。如果新类型没有覆盖所有调用其他基本操作的基本操作,则新类型可能无法按预期工作。最安全的策略是对对象使用类范围转换以强制分派调用。实现此目标的一种方法是在每个分派方法中定义一个类范围常量

 procedure Op2 (This: Derived) is
   This_Class: constant Root'Class := This;
 begin

This用于访问数据项并进行任何非分派调用。This_Class用于进行分派调用。

不太常见,或许不那么令人惊讶的是,来自针对标记类型的不可分派(类范围)例程对同一对象上的其他例程的调用默认情况下是分派的

 type Root is tagged private;
 procedure Op1 (This: Root'Class);
 procedure Op2 (This: Root);

 type Derived is new Root with private;
 -- Derived does not inherit Op1, rather Op1 is applicable to Derived.
 overriding procedure Op2 (This: Derived);

 procedure Op1 (This: Root'Class) is
 begin
   ...
   Op2 (This);               -- dispatching
   Op2 (Root (This));        -- static call
   This.Op2;                 -- dispatching (new syntax since Ada 2005)
   (Root (This)).Op2;        -- static call (new syntax since Ada 2005)
   ...
 end Op1;

 D: Derived;
 C: Root'Class := D;

 Op1 (D);  -- static call
 Op1 (C);  -- static call
 D.Op1;    -- static call (new syntax since Ada 2005)
 C.Op1;    -- static call (new syntax since Ada 2005)

请注意,对Op1的调用始终是静态的,因为Op1没有被继承。它的参数类型是类范围类型,因此该操作适用于从 Root 派生的所有类型。(Op2 在分派表中为从Root派生的每个类型都有一项条目。Op1没有这样的分派表;相反,所有类型只有一个这样的操作。)

来自Op1的正常调用是分派的,因为This的声明类型是类范围类型。分派的默认值通常不会造成麻烦,因为类范围操作通常用于执行涉及对一个或多个分派操作的调用的脚本。

运行时类型识别

[edit | edit source]

运行时类型识别允许程序在运行时(间接或直接)查询对象的标记以确定该对象属于哪个类型。此功能显然只有在多态性和动态分派的情况下才有意义,因此仅适用于标记类型。

您可以通过成员测试来确定对象是否属于某个类型类或特定类型in,例如

type Base    is tagged private;
type Derived is new Base    with private;
type Leaf    is new Derived with private;

...
procedure Explicit_Dispatch (This : in Base'Class) is
begin
   if This in Leaf then ... end if;
   if This in Derived'Class then ... end if;
end Explicit_Dispatch;

由于 Ada 的强类型规则,运行时类型识别实际上很少需要;类范围类型和特定类型之间的区别通常允许程序员确保对象是适当的类型,而无需使用此功能。

此外,参考手册定义了package Ada.Tags(RM 3.9(6/2))、属性'Tag(RM 3.9(16,18))和function Ada.Tags.Generic_Dispatching_Constructor(RM 3.9(18.2/2)),它们使直接操作标记成为可能。

创建对象

[edit | edit source]

语言参考手册中有关3.3:对象和命名数字 [注释]的部分说明了何时创建对象以及何时再次销毁对象。本小节说明了如何创建对象。

LRM 部分开头写着:

对象在运行时创建,并包含给定类型的值。对象可以在细化声明、评估分配器、聚合或函数调用时创建和初始化.

例如,假设一个典型的面向对象类型层次结构:一个顶层类型Person、一个从Person派生的Programmer类型,以及可能更多的其他人类型。每个人都有一个名字;假设Person对象具有一个Name组件。同样,每个人都有一个Gender组件。Programmer类型继承了Person类型的组件和操作,因此Programmer对象也具有一个Name和一个Gender组件。Programmer对象可能具有特定于程序员的附加组件。

标记类型的对象与任何类型的对象以相同的方式创建。LRM 的第二个句子说,例如,当您声明一个类型变量或常量时,将创建一个对象。对于标记类型Person

declare
   P: Person;
begin
   Text_IO.Put_Line("The name is " & P.Name);
end;

到目前为止没有特殊之处。就像任何普通的变量声明一样,这个 O-O 变量被细化了。细化结果是一个名为P的类型为Person的对象。但是,P只有默认的姓名和性别值组件。这些可能不是有用的值。为对象组件提供初始值的一种方法是分配一个聚合。

declare
   P: Person := (Name => "Scorsese", Gender => Male);
begin
   Text_IO.Put_Line("The name is " & P.Name);
end;

:=后的括号表达式称为聚合4.3:聚合 [注释])。

LRM 段落中提到的另一种创建对象的 方法是调用函数。将创建一个对象作为函数调用的返回值。因此,我们可以调用一个返回对象的函数,而不是使用初始值的聚合。

引入适当的 O-O 信息隐藏,我们更改包含Person类型的包,以便Person成为一个私有类型。为了使包的客户端能够构造Person对象,我们声明一个返回它们的函数。(该函数可能会对对象执行一些有趣的构造工作。例如,上面的聚合很可能会根据提供的姓名字符串引发 Constraint_Error 异常;该函数可以对姓名进行混淆,使其与组件的声明相匹配。)我们还声明一个返回Person对象姓名的函数。

package Persons is

   type Person is tagged private;

   function Make (Name: String; Sex: Gender_Type) return Person;

   function Name (P: Person) return String;

private
   type Person is tagged
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Persons;

调用Make函数会产生一个可用于初始化的对象。由于Person类型是私有的,因此我们不能再引用PName组件。但是,有一个对应的函数Name被声明为类型Person,使其成为所谓的原始操作。(此示例中的组件和函数都名为Name 但是,如果需要,我们可以为两者选择不同的名称。)

declare
   P: Person := Make (Name => "Orwell", Sex => Male);
begin
   Text_IO.Put_Line("The name is " & Name(P));
end;

对象可以复制到另一个对象中。目标对象首先被销毁。然后,源对象的组件值被分配给目标对象的相应组件。在以下示例中,默认初始化的P获得从Make调用创建的对象之一的副本。

declare
   P: Person;
begin
   if 2001 > 1984 then
      P := Make (Name => "Kubrick", Sex => Male);
   else
      P := Make (Name => "Orwell", Sex => Male);
   end if;

   Text_IO.Put_Line("The name is " & Name(P));
end;

到目前为止,还没有提到从Person派生的Programmer类型。还没有多态性,同样,初始化也没有提到继承。在处理Programmer对象及其初始化之前,有必要简单介绍一下类范围类型。

关于基本操作的更多详细信息

[edit | edit source]

请记住我们之前对"基本操作" 的说法。基本操作是

  • 接受标记类型参数的子程序;
  • 返回标记类型对象的函数;
  • 接受指向标记类型的匿名访问类型的子程序;
  • 仅在 Ada 2005 中,返回指向标记类型的匿名访问类型的函数;

此外,基本操作必须在类型冻结之前声明(冻结的概念将在后面解释)

示例

package X is
   type Object is tagged null record;

   procedure Primitive_1 (This : in     Object);
   procedure Primitive_2 (That :    out Object);
   procedure Primitive_3 (Me   : in out Object);
   procedure Primitive_4 (Them : access Object);
   function  Primitive_5 return Object;
   function  Primitive_6 (Everyone : Boolean) return access Object;
end X;

所有这些子程序都是基本操作。

基本操作还可以接受相同类型或其他类型的参数;此外,控制操作数不必是第一个参数

package X is
   type Object is tagged null record;

   procedure Primitive_1 (This : in Object; Number : in Integer);
   procedure Primitive_2 (You  : in Boolean; That : out Object);
   procedure Primitive_3 (Me, Her : in out Object);
end X;

基本操作的定义明确排除了命名访问类型和类范围类型,以及不在同一声明区域中立即定义的操作。反例

package X is
   type Object is tagged null record;
   type Object_Access is access Object;
   type Object_Class_Access is access Object'Class;

   procedure Not_Primitive_1 (This : in     Object'Class);
   procedure Not_Primitive_2 (This : in out Object_Access);
   procedure Not_Primitive_3 (This :    out Object_Class_Access);
   function  Not_Primitive_4 return Object'Class;

   package Inner is
       procedure Not_Primitive_5 (This : in Object);
   end Inner;
end X;

高级主题:冻结规则

[edit | edit source]

冻结规则(ARM 13.14)可能是 Ada 语言定义中最复杂的部分;这是因为该标准试图尽可能明确地描述冻结。此外,语言定义的这一部分涉及所有实体的冻结,包括复杂的场景,如泛型和通过取消引用访问值访问的对象。但是,如果您了解动态分派的工作原理,您可以直观地了解标记类型的冻结。在那一节中,我们看到编译器为每个标记类型发出一个基本操作表。程序文本中发生此事件的点是标记类型冻结的点,即表变得完整的点。类型冻结后,就不能再向其中添加基本操作。

此点是以下最早的点:

  • 声明标记类型的包规范的末尾
  • 从标记类型派生的第一个类型的出现

示例

package X is

  type Object is tagged null record;
  procedure Primitive_1 (This: in Object);

  -- this declaration freezes Object
  type Derived is new Object with null record;

  -- illegal: declared after Object is frozen
  procedure Primitive_2 (This: in Object);

end X;

直观地:在声明 Derived 的时候,编译器开始为派生类型创建一个新的基本操作表。最初,此新表等于父类型Object的基本操作表。因此,Object必须冻结。

  • 标记类型变量的声明

示例

package X is

  type Object is tagged null record;
  procedure Primitive_1 (This: in Object);

  V: Object;  -- this declaration freezes Object

  -- illegal: Primitive operation declared after Object is frozen
  procedure Primitive_2 (This: in Object);

end X;

直观地:在声明V之后,就可以在V上调用该类型的任何基本操作。因此,基本操作列表必须已知且完整,即冻结。

  • 带标记类型的常量的完成(不是声明,如果有的话)
package X is

  type Object is tagged null record;
  procedure Primitive_1 (This: in Object);

  -- this declaration does NOT freeze Object
  Deferred_Constant: constant Object;

  procedure Primitive_2 (This : in Object); -- OK

private

  -- only the completion freezes Object
  Deferred_Constant: constant Object := (null record);

  -- illegal: declared after Object is frozen
  procedure Primitive_3 (This: in Object);

 end X;

Ada 2005 的新特性

[edit | edit source]

此语言特性仅从 Ada 2005 开始可用。

Ada 2005 添加了覆盖指示器,允许在更多地方使用匿名访问类型,并提供 object.method 表示法。

覆盖指示器

[edit | edit source]

新关键字overriding 可用于指示操作是否覆盖继承的子程序。由于与 Ada 95 的向上兼容性,它的使用是可选的。例如

package X is
    type Object is tagged null record;

   function  Primitive return access Object; -- new in Ada 2005

   type Derived_Object is new Object with null record;

   not overriding -- new optional keywords in Ada 2005
   procedure Primitive (This : in Derived_Object); -- new primitive operation

   overriding
   function  Primitive return access Derived_Object;
end X;

编译器将检查所需的行为。

这是一个良好的编程实践,因为它可以避免一些讨厌的错误,例如由于程序员拼写标识符错误,或者由于后来在父类型中添加了新的参数而导致没有覆盖继承的子程序。

它也可以用于抽象操作、重命名或实例化泛型子程序

not overriding
procedure Primitive_X (This : in Object) is abstract;

overriding
function  Primitive_Y return Object renames Some_Other_Subprogram;

not overriding
procedure Primitive_Z (This : out Object)
      is new Generic_Procedure (Element => Integer);

Object.Method 表示法

[edit | edit source]

我们已经看到了这种表示法

package X is
   type Object is tagged null record;

   procedure Primitive (This: in Object; That: in Boolean);
end X;
with X;
procedure Main is
   Obj : X.Object;
begin
   Obj.Primitive (That => True); -- Ada 2005 object.method notation
end Main;

这种表示法仅适用于控制参数是第一个参数的原始操作。

抽象类型

[edit | edit source]

带标记类型也可以是抽象的(因此可以有抽象操作)

package X is

   type Object is abstract tagged …;

   procedure One_Class_Member      (This : in     Object);
   procedure Another_Class_Member  (This : in out Object);
   function  Abstract_Class_Member return Object  is abstract;

end X;

抽象操作不能有任何主体,因此派生类型被迫覆盖它(除非这些派生类型也是抽象的)。有关这方面的更多信息,请参阅下一节关于接口的内容。

与非抽象带标记类型不同的是,你不能声明任何此类型的变量。但是,你可以声明对它的访问,并将其用作类范围操作的参数。

通过接口实现多重继承

[edit | edit source]

此语言特性仅从 Ada 2005 开始可用。

接口允许有限形式的多重继承(取自 Java)。从语义上讲,它们类似于“抽象带标记的空记录”,因为它们可能具有原始操作,但不能保存任何数据,因此这些操作不能有主体,它们要么被声明为abstractnull抽象表示操作必须被覆盖,表示默认实现为空主体,即不执行任何操作的实现。

接口用以下方式声明

package Printable is
   type Object is interface;
   procedure Class_Member_1 (This : in     Object) is abstract;
   procedure Class_Member_2 (This :    out Object) is null;
end Printable;

你通过将其添加到具体的中来实现一个interface

with Person;
package Programmer is
   type Object is new Person.Object
                  and Printable.Object
   with
      record
         Skilled_In : Language_List;
      end record;
   overriding
   procedure Class_Member_1   (This : in Object);
   not overriding
   procedure New_Class_Member (This : Object; That : String);
end Programmer;

像往常一样,所有继承的抽象操作都必须被覆盖,尽管空子程序不需要。

这种类型可以实现接口列表(称为祖先),但只能有一个父类。父类可以是具体类型,也可以是接口。

type Derived is new Parent and Progenitor_1 and Progenitor_2 ... with ...;

通过 mix-in 实现多重继承

[edit | edit source]

Ada 支持对接口的多重继承(见上文),但只支持对实现的单一继承。这意味着带标记类型可以实现多个接口,但只能扩展一个祖先带标记类型。

如果你想将行为添加到一个已经扩展了另一个类型的类型,这可能会成为问题;例如,假设你有

type Base is tagged private;
type Derived is new Base with private;

并且你想使Derived 受控,即添加Derived 控制其初始化、赋值和终结的行为。可惜你不能写

type Derived is new Base and Ada.Finalization.Controlled with private; -- illegal

因为Ada.Finalization 由于历史原因,没有定义接口ControlledLimited_Controlled,而是定义了抽象类型。

如果你的基本类型不是受限的,那么没有很好的解决方案;你必须回到类的根部并使它受控。(原因将在稍后变得很明显。)

但是,对于受限类型,另一种解决方案是使用 mix-in

type Base is tagged limited private;
type Derived;

type Controlled_Mix_In (Enclosing: access Derived) is
  new Ada.Finalization.Limited_Controlled with null record;

overriding procedure Initialize (This: in out Controlled_Mix_In);
overriding procedure Finalize   (This: in out Controlled_Mix_In);

type Derived is new Base with record
  Mix_In: Controlled_Mix_In (Enclosing => Derived'Access); -- special syntax here
  -- other components here...
end record;

这种特殊类型的 mix-in 是一个对象,它有一个访问判别式,该判别式引用其封闭对象(也称为Rosen 技巧)。在Derived 类型的声明中,我们使用特殊语法初始化此判别式:Derived'Access 实际上引用的是Derived 类型当前实例的访问值。因此,访问判别式允许 mix-in 查看其封闭对象及其所有组件;因此它可以初始化和终结其封闭对象

overriding procedure Initialize (This: in out Controlled_Mix_In) is
  Enclosing: Derived renames This.Enclosing.all;
begin
  -- initialize Enclosing...
end Initialize;

以及Finalize 的类似情况。

这对于非受限类型不起作用的原因是通过判别式的自引用。想象一下,你拥有两个这样的非受限类型的变量,并将一个赋值给另一个

X := Y;

在赋值语句中,Adjust 只在XFinalize 之后被调用,因此不能提供判别式的新的值。因此X.Mixin_In.Enclosing 将不可避免地引用Y

现在让我们进一步扩展我们的层次结构

type Further is new Derived with null record;

overriding procedure Initialize (This: in out Further);
overriding procedure Finalize   (This: in out Further);

哎呀,这不起作用,因为还没有Derived 的相应过程 - 因此让我们快速添加它们。

type Base is tagged limited private;
type Derived;

type Controlled_Mix_In (Enclosing: access Derived) is
  new Ada.Finalization.Limited_Controlled with null record;

overriding procedure Initialize (This: in out Controlled_Mix_In);
overriding procedure Finalize   (This: in out Controlled_Mix_In);

type Derived is new Base with record
  Mix_In: Controlled_Mix_In (Enclosing => Derived'Access);  -- special syntax here
  -- other components here...
end record;

not overriding procedure Initialize (This: in out Derived);  -- sic, they are new
not overriding procedure Finalize   (This: in out Derived);

type Further is new Derived with null record;
overriding procedure Initialize (This: in out Further);
overriding procedure Finalize   (This: in out Further);

当然,我们必须为Derived 上的过程写入not overriding,因为它们实际上没有可以覆盖的内容。主体是

not overriding procedure Initialize (This: in out Derived) is
begin
  -- initialize Derived...
end Initialize;
overriding procedure Initialize (This: in out Controlled_Mix_In) is
  Enclosing: Derived renames This.Enclosing.all;
begin
  Initialize (Enclosing);
end Initialize;

令我们沮丧的是,我们必须了解到,类型为Further 的对象的Initialize/Finalize 不会被调用,而是会调用其父类DerivedInitialize/Finalize。为什么?

declare
  X: Further;  -- Initialize (Derived (X)) is called here
begin
  null;
end;  -- Finalize (Derived (X)) is called here

原因是 mix-in 在上面的重命名语句中定义了局部对象Enclosing 的类型为Derived。为了解决这个问题,我们必须使用令人讨厌的重新分派(以不同的但等效的表示法显示)

overriding procedure Initialize (This: in out Controlled_Mix_In) is
  Enclosing: Derived renames This.Enclosing.all;
begin
  Initialize (Derived'Class (Enclosing));
end Initialize;
overriding procedure Finalize (This: in out Controlled_Mix_In) is
  Enclosing: Derived'Class renames Derived'Class (This.Enclosing.all);
begin
  Enclosing.Finalize;
end Finalize;
declare
  X: Further;  -- Initialize (X) is called here
begin
  null;
end;  -- Finalize (X) is called here

或者(可能更好)写

type Controlled_Mix_In (Enclosing: access Derived'Class) is
  new Ada.Finalization.Limited_Controlled with null record;

然后我们自动获得重新分派,并且可以省略Enclosing 上的类型转换。

类名

[edit | edit source]

类包和类记录都需要一个名称。理论上它们可以有相同的名称,但实际上,当使用use 子句时,这会导致讨厌的(由于直观错误消息)名称冲突。因此,随着时间的推移,三种事实上的命名标准已被广泛使用。

类/类

[edit | edit source]

包以复数名词命名,记录以相应的单数形式命名。

package Persons is

   type Person is tagged
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Persons;

此约定是 Ada 内置库中通常使用的约定。

缺点:一些“复数”很难拼写,尤其是对于那些不是以英语为母语的人来说。

类/对象

[edit | edit source]

包以类命名,记录只命名为 Object。

package Person is

   type Object is tagged
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Person;

大多数 UMLIDL 代码生成器使用这种技术。

缺点:你不能在任何时候对多个这样的类包使用use 子句。但是,你始终可以使用“类型”而不是包。

类/类_类型

[edit | edit source]

包以类命名,记录以_Type 为后缀。

package Person is

   type Person_Type is tagged
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Person;

缺点:很多难看的“_Type”后缀。

面向对象的 Ada 适用于 C++ 程序员

[edit | edit source]

在 C++ 中,结构

 struct C {
   virtual void v();
   void w();
   static void u();
 };

与 Ada 中的以下内容严格等效

package P is
  type C is tagged null record;
  procedure V (This : in out C);        -- primitive operation, will be inherited upon derivation
  procedure W (This : in out C'Class);  -- not primitive, will not be inherited upon derivation
  procedure U;
end P;

在 C++ 中,成员函数隐式地接受一个类型为 C*this 参数。在 Ada 中,所有参数都是显式的。因此,u() 接受参数这一事实,在 C++ 中是隐式的,而在 Ada 中是显式的。

在 C++ 中,this 是一个指针。在 Ada 中,显式的 This 参数不必是指针;所有标记类型参数都隐式地按引用传递。

静态分派

[edit | edit source]

在 C++ 中,函数调用在以下情况下静态分派

  • 调用的目标是对象类型
  • 成员函数是非虚拟的

例如

 C object;
 object.v();
 object.w();

都静态分派。特别是,对 v() 的静态分派可能会令人困惑;这是因为对象既不是指针也不是引用。Ada 在这方面行为完全相同,只是 Ada 将此称为静态绑定而不是分派

declare
   Object : P.C;
begin
   Object.V; -- statically bound
   Object.W; -- statically bound
end;

动态分派

[edit | edit source]

在 C++ 中,如果同时满足以下两个条件,函数调用会动态分派

  • 调用的目标是指针或引用
  • 成员函数是虚拟的。

例如

 C* object;
 object->v(); // dynamic dispatch
 object->w(); // static, non-virtual member function
 object->u(); // illegal: static member function
 C::u(); // static dispatch

在 Ada 中,原始子程序调用(动态地)分派,当且仅当

  • 目标对象是类范围类型;

注意:在 Ada 行话中,术语分派始终表示动态

例如

declare
   Object : P.C'Class := ...;
begin
   P.V (Object); -- dispatching
   P.W (Object); -- statically bound: not a primitive operation
   P.U; -- statically bound
end;

如您所见,不需要访问类型或指针来在 Ada 中进行分派。在 Ada 中,标记类型总是按引用传递给子程序,而不需要显式的访问值。

还要注意,在 C++ 中,类充当

  • 封装单元(Ada 使用包和可见性来实现这一点)
  • 类型,如 Ada 中一样。

因此,您在 C++ 中调用 C::u(),因为 u() 封装在 C 中,但在 Ada 中调用 P.U,因为 U 封装在 P 中,而不是类型 C 中。

类范围类型和特定类型

[edit | edit source]

对于 C++ 程序员来说,最令人困惑的部分是“类范围类型”的概念。为了帮助您理解

  • C++ 中的指针和引用实际上是隐式的类范围;
  • C++ 中的对象类型实际上是特定的;
  • C++ 不提供声明等效于以下内容的方法
type C_Specific_Access is access C;
  • C++ 不提供声明等效于以下内容的方法
type C_Specific_Access_One is access C;
type C_Specific_Access_Two is access C;

这在 Ada 中是两种不同的、不兼容的类型,可能从不同的存储池分配它们的内存!

  • 在 Ada 中,您不需要访问值来进行动态分派。
  • 在 Ada 中,您使用访问值来进行动态内存管理(仅限),使用类范围类型来进行动态分派(仅限)。
  • 在 C++ 中,您使用指针和引用来进行动态内存管理和动态分派。
  • 在 Ada 中,类范围类型是显式的(使用 'Class)。
  • 在 C++ 中,类范围类型是隐式的(使用 *&)。

构造函数

[edit | edit source]

在 C++ 中,一个特殊的语法声明了一个构造函数

 class C {
    C(/* optional parameters */); // constructor
 };

构造函数不能是虚拟的。一个类可以根据需要拥有任意数量的构造函数,这些构造函数由它们的形参区分。

Ada 没有这样的构造函数。它们可能被认为是不必要的,因为在 Ada 中,任何返回标记类型对象的函数都可以充当一种构造函数。然而,这与 C++ 中的真实构造函数不同;这种区别在派生树的情况下最为明显(参见下面的 Finalization)。Ada 构造函数子程序不必具有特殊的名称,并且可以根据需要拥有任意数量的构造函数;每个函数都可以根据需要接受形参。

package P is
  type T is tagged private;
  function Make                 return T;  -- constructor
  function To_T (From: Integer) return T;  -- another constructor
  procedure Make (This: out T);            -- not a constructor
private
  ...
end P;

如果 Ada 构造函数也是一个原始操作(如上面的示例),那么它在派生时会成为抽象的,并且如果派生类型本身不是抽象的,则必须重写它。如果您不希望这样,请在嵌套范围内声明此类函数。

在 C++ 中,一种惯例是复制构造函数及其同类赋值运算符

 class C {
    C(const C& that); // copies "that" into "this"
    C& operator= (const C& right); // assigns "right" to "this", which is "left"
 };

该复制构造函数在初始化时隐式调用,例如

 C a = b; // calls the copy constructor
 C c;
 a = c;   // calls the assignment operator

Ada 通过受控类型提供类似的功能。受控类型是扩展了预定义类型 Ada.Finalization.Controlled 的类型

with Ada.Finalization;
package P is
  type T is new Ada.Finalization.Controlled with private;
  function Make return T;  -- constructor
private
  type T is ... end record;
  overriding procedure Initialize (This: in out T);
  overriding procedure Adjust     (This: in out T); -- copy constructor
end P;

请注意,Initialize 不是构造函数;它在某种程度上类似于 C++ 构造函数,但也有很大不同。假设您有一个从 T 派生的类型 T1,其中包含对 Initialize 的适当重写。一个真实的构造函数(如 C++ 中的构造函数)会自动首先构造父组件(T),然后构造子组件。在 Ada 中,这不是自动的。为了在 Ada 中模拟这一点,我们必须编写

procedure Initialize (This: in out T1) is
begin
  Initialize (T (This));  -- Don't forget this part!
  ...  -- handle the new components here
end Initialize;

编译器在每个类型为 T 的对象在未给出初始值时被分配时,会在每个对象之后插入对 Initialize 的调用。它还会在对对象的每次赋值后插入对 Adjust 的调用。因此,声明

A: T;
B: T := X;

  • 为 A 分配内存
  • 调用 Initialize (A)
  • 为 B 分配内存
  • 将 X 的内容复制到 B
  • 调用 Adjust (B)

由于显式初始化,不会调用 Initialize (B)。

因此,复制构造函数的等效项是对 Adjust 的重写。

如果您想为扩展另一个非受控类型的类型提供此功能,请参见 "多重继承"

析构函数

[edit | edit source]

在 C++ 中,析构函数是一个仅具有隐式 this 形参的成员函数

 class C {
    virtual ~C(); // destructor
 }

构造函数不能是虚拟的,而析构函数必须是虚拟的,如果该类要与动态分派一起使用(具有虚拟方法或从具有虚拟方法的类派生)。C++ 类默认情况下不使用动态分派,因此它可能会通过简单地忘记关键字 virtual 来捕获一些程序员,并在他们的程序中造成破坏。

在 Ada 中,等效的功能再次由受控类型提供,通过重写过程 Finalize

with Ada.Finalization;
package P is
   type T is new Ada.Finalization.Controlled with private;
   function Make return T;  -- constructor
private
   type T is ... end record;
   overriding procedure Finalize (This: in out T);  -- destructor
end P;

由于 Finalize 是一个原始操作,因此它是自动“虚拟”的;在 Ada 中,您不能忘记使析构函数成为虚拟的。

封装:公共、私有和受保护的成员

[edit | edit source]

在 C++ 中,封装的单元是类;在 Ada 中,封装的单元是包。这对 Ada 程序员如何放置对象类型的各个组件有影响。

 class C {
 public:
    int a;
    void public_proc();
 protected:
    int b;
    int protected_func();
 private:
    bool c;
    void private_proc();
 };

在 Ada 中模拟 C++ 类的另一种方法是定义一个类型层次结构,其中基类型是公共部分,它必须是抽象的,这样才能防止定义这种基类型的独立对象。它看起来像这样

private with Ada.Finalization;

package CPP is

  type Public_Part is abstract tagged record  -- no objects of this type
    A: Integer;
  end record;

  procedure Public_Proc (This: in out Public_Part);

  type Complete_Type is new Public_Part with private;

  -- procedure Public_Proc (This: in out Complete_Type);  -- inherited, implicitly defined

private  -- visible for children

  type Private_Part;  -- declaration stub
  type Private_Part_Pointer is access Private_Part;

  type Private_Component is new Ada.Finalization.Controlled with record
    P: Private_Part_Pointer;
  end record;

  overriding procedure Initialize (X: in out Private_Component);
  overriding procedure Adjust     (X: in out Private_Component);
  overriding procedure Finalize   (X: in out Private_Component);

  type Complete_Type is new Public_Part with record
    B: Integer;
    P: Private_Component;  -- must be controlled to avoid storage leaks
  end record;

  not overriding procedure Protected_Proc (This: Complete_Type);

end CPP;

私有部分仅定义为存根,其完成隐藏在主体中。为了使它成为完整类型的组件,我们必须使用指针,因为组件的大小仍然未知(指针的大小对编译器来说是已知的)。不幸的是,使用指针,我们会冒内存泄漏的风险,因此我们必须使私有组件受控。

为了进行一个小测试,这是主体,其中子程序主体通过识别打印提供。

with Ada.Unchecked_Deallocation;
with Ada.Text_IO;

package body CPP is

  procedure Public_Proc (This: in out Public_Part) is  -- primitive
  begin
    Ada.Text_IO.Put_Line ("Public_Proc" & Integer'Image (This.A));
  end Public_Proc;

  type Private_Part is record  -- complete declaration
    C: Boolean;
  end record;

  overriding procedure Initialize (X: in out Private_Component) is
  begin
    X.P := new Private_Part'(C => True);
    Ada.Text_IO.Put_Line ("Initialize " & Boolean'Image (X.P.C));
  end Initialize;

  overriding procedure Adjust (X: in out Private_Component) is
  begin
    Ada.Text_IO.Put_Line ("Adjust " & Boolean'Image (X.P.C));
    X.P := new Private_Part'(C => X.P.C);  -- deep copy
  end Adjust;

  overriding procedure Finalize (X: in out Private_Component) is
    procedure Free is new Ada.Unchecked_Deallocation (Private_Part, Private_Part_Pointer);
  begin
    Ada.Text_IO.Put_Line ("Finalize " & Boolean'Image (X.P.C));
    Free (X.P);
  end Finalize;

  procedure Private_Proc (This: in out Complete_Type) is  -- not primitive
  begin
    Ada.Text_IO.Put_Line ("Private_Proc" & Integer'Image (This.A) & Integer'Image (This.B) & ' ' & Boolean'Image (This.P.P.C));
  end Private_Proc;

  not overriding procedure Protected_Proc (This: Complete_Type) is  -- primitive
    X: Complete_Type := This;
  begin
    Ada.Text_IO.Put_Line ("Protected_Proc" & Integer'Image (This.A) & Integer'Image (This.B));
    Private_Proc (X);
  end Protected_Proc;

end CPP;

我们看到,由于构造,私有过程不是一个原始操作。

让我们定义一个子类,以便可以访问受保护的操作

package CPP.Child is
 
  procedure Do_It (X: Complete_Type);  -- not primitive

end CPP.Child;

子类可以查看父类的私有部分,因此可以查看受保护的过程

with Ada.Text_IO;

package body CPP.Child is

  procedure Do_It (X: Complete_Type) is
  begin
    Ada.Text_IO.Put_Line ("Do_It" & Integer'Image (X.A) & Integer'Image (X.B));
    Protected_Proc (X);
  end Do_It;

end CPP.Child;

这是一个简单的测试程序,其输出显示在下面。

with CPP.Child;
use  CPP.Child, CPP;

procedure Test_CPP is

  X, Y: Complete_Type;

begin

  X.A := +1;
  Y.A := -1;

  Public_Proc (X);  Do_It (X);
  Public_Proc (Y);  Do_It (Y);

  X := Y;

  Public_Proc (X);  Do_It (X);

end Test_CPP;

这是测试程序的注释输出

Initialize TRUE                     Test_CPP: Initialize X
Initialize TRUE                                      and Y
Public_Proc 1                       |  Public_Proc (X):  A=1
Do_It 1-1073746208                  |  Do_It (X):        B uninitialized
Adjust TRUE                         |  |  Protected_Proc (X): Adjust local copy X of This
Protected_Proc 1-1073746208         |  |  |
Private_Proc 1-1073746208 TRUE      |  |  |  Private_Proc on local copy of This
Finalize TRUE                       |  |  Protected_Proc (X): Finalize local copy X
Public_Proc-1                       |  ditto for Y
Do_It-1 65536                       |  |
Adjust TRUE                         |  |
Protected_Proc-1 65536              |  |
Private_Proc-1 65536 TRUE           |  |
Finalize TRUE                       |  |
Finalize TRUE                       |  Assignment: Finalize target X.P.C
Adjust TRUE                         |  |           Adjust: deep copy
Public_Proc-1                       |  again for X, i.e. copy of Y
Do_It-1 65536                       |  |
Adjust TRUE                         |  |
Protected_Proc-1 65536              |  |
Private_Proc-1 65536 TRUE           |  |
Finalize TRUE                       |  |
Finalize TRUE                       Finalize Y
Finalize TRUE                            and X

您看到将 C++ 行为直接翻译成 Ada 是很困难的,即使可行。我认为,原始 Ada 子程序更像是虚拟的 C++ 方法(在本例中,它们不是)。每种语言都有其自身的特性,必须考虑这些特性,因此尝试将代码从一种语言直接翻译成另一种语言可能不是最好的方法。

反封装:朋友和流输入输出

[edit | edit source]

在 C++ 中,朋友函数或类可以查看它所成为朋友的类的所有成员。朋友会破坏封装,因此应该避免使用。在 Ada 中,由于包而不是类是封装的单元,因此“朋友”子程序只是在与标记类型相同的包中声明的子程序。

在 C++ 中,流输入输出是通常需要朋友的特殊情况

 #include <iostream>
 class C {
 public:
    C();
    friend ostream& operator<<(ostream& output, C& arg);
 private:
    int a, b;
    bool c;
 };

 #include <iostream>
 int main() {
    C object;
    cout << object;
    return 0;
 };

Ada 不需要这种构造,因为它默认定义流输入和输出操作:可以重写 InputOutputReadWrite 属性的默认实现(以 Write 为例)。重写必须在类型冻结之前发生,即(在本示例的情况下)在包规范中。

private with Ada.Streams;  -- needed only in the private part
package P is
   type C is tagged private;
private
   type C is tagged record
      A, B : Integer;
      C : Boolean;
   end record;
   procedure My_Write (Stream : not null access Ada.Streams.Root_Stream_Type'Class;
                       Item   : in C);
   for C'Write use My_Write;  -- override the default attribute
end P;

默认情况下,Write 属性会按照声明中给出的顺序将组件发送到流,即 A、B 然后 C,因此我们更改了顺序。

package body P is
   procedure My_Write (Stream : not null access Ada.Streams.Root_Stream_Type'Class;
                       Item : in C) is
   begin
      -- The default implementation is to write A then B then C; here we change the order.
      Boolean'Write (Stream, Item.C);  -- call the
      Integer'Write (Stream, Item.B);  --   default attributes
      Integer'Write (Stream, Item.A);  --      for the components
   end My_Write;
end P;

现在 P.C'Write 调用包的重写版本。

 with Ada.Text_IO.Text_Streams;
 with P;
 procedure Main is
    Object : P.C;
 begin
    P.C'Write (Ada.Text_IO.Text_Streams.Stream (Ada.Text_IO.Standard_Output),
               Object);
 end Main;

请注意,流 IO 属性不是标记类型的原始操作;在 C++ 中也是如此,朋友运算符实际上不是类型的成员函数。

术语

[edit | edit source]
Ada C++
类(作为封装单元)
带标记类型 类(对象)(作为类型)( *不* 指针或引用,它们是类范围的)
基本操作 虚成员函数
标记 指向虚表的指针
类(类型) 一个类树,以基类为根,包括该基类的所有(递归)派生类
类范围类型 -
类范围操作 静态成员函数
访问特定带标记类型的值 -
访问类范围类型的值 指向类的指针或引用

维基教科书

[编辑 | 编辑源代码]

维基百科

[编辑 | 编辑源代码]

Ada 参考手册

[编辑 | 编辑源代码]

Ada 质量和风格指南

[编辑 | 编辑源代码]
华夏公益教科书