Ada 编程/面向对象
面向对象编程是指以“对象”为单位构建软件。一个“对象”包含数据并具有行为。数据通常由常量和变量组成,如本书其他部分所述,但也可以在程序之外,例如磁盘或网络上。行为由对数据进行操作的子程序组成。与过程式编程相比,面向对象编程的独特之处不在于单个特性,而是几个特性的组合。
- 封装,即能够将对象的实现与其接口分离;这反过来又将对象的“客户端”(只能以某些预定义方式使用对象)与对象的内部(对外部客户端一无所知)分离。
- 继承,一种类型的对象能够继承另一种类型的对象的數據和行为(子程序),而无需打破封装;
- 类型扩展,对象能够在继承的对象的基础上添加新的数据组件和新的子程序,并用自己的版本替换一些继承的子程序;这称为覆盖。
- 多态性,"客户端"能够在不知道对象的确切类型的情况下使用对象的服務,即以抽象的方式。实际上,在运行时,实际对象在每次调用时可能具有不同的类型。
任何语言都可以进行面向对象编程,即使是汇编语言。然而,如果没有语言支持,类型扩展和多态性很难实现。
在 Ada 中,每个概念都有一个匹配的构造;这就是 Ada 直接支持面向对象编程的原因。
- 包提供封装;
- 派生类型提供继承;
- 记录扩展(如下所述)提供类型扩展;
- 类范围类型(如下所述)提供多态性。
Ada 从第一个版本(1980 年的 MIL-STD-1815)开始就拥有封装和派生类型,这导致一些人以非常狭义的意义将该语言归类为“面向对象”的。记录扩展和类范围类型是在 Ada 95 中添加的。Ada 2005 进一步增加了接口。本章的其余部分将涵盖这些方面。
package
Directoryis
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 的基本操作至少需要有一个类型为T
或access T
的参数。虽然大多数面向对象语言会自动提供this
或self
指针,但 Ada 要求显式声明一个形式参数来接收当前对象。该参数通常是参数列表中的第一个参数,它允许object.subprogram
调用语法(从 Ada 2005 开始可用),但它可以位于任何参数位置。带标记类型始终按引用传递;参数传递方式与参数模式in
和out
无关,这些模式描述了数据流。对于T
和access T
,参数传递方式相同。
对于带标记类型,参数列表中不能使用其他直接可分派类型,因为 Ada 不提供多重分派。以下示例是非法的。
package
Pis
type
Ais
tagged
private
;type
Bis
tagged
private
;procedure
Proc (This: B; That: A); -- illegal: can't dispatch on both A and Bend
P;
当需要传递额外的可分派对象时,参数列表应使用它们的类范围类型T'Class
来声明它们。例如
package
Pis
type
Ais
tagged
private
;type
Bis
tagged
private
;procedure
Proc (This: B; That: A'Class); -- dispatching only on Bend
P;
但是,这并不限制相同带标记类型参数的数量。例如,以下定义是合法的。
package
Pis
type
Ais
tagged
private
;procedure
Proc (This, That: A); -- dispatching only on Aend
P;
带标记类型的基本操作是分派操作。对这种基本操作的调用实际上是分派调用还是静态绑定,取决于上下文(见下文)。请注意,在分派调用中,最后一个示例的两个实际参数必须具有相同的标记(即相同的类型);如果标记检查失败,将调用 Constraint_Error。
类型派生一直是 Ada 的核心部分。
package
Pis
type
Tis private
;function
Create (Data: Boolean)return
T; -- primitiveprocedure
Work (Object :in out
T); -- primitiveprocedure
Work (Pointer:access
T); -- primitivetype
Acc_Tis access
T;procedure
Proc (Pointer: Acc_T); -- not primitiveprivate
type
Tis record
Data: Boolean;end record
;end
P;
上面的示例创建了一个包含数据(这里只是一个布尔值,但可以是任何东西)和行为的类型 T,行为包括一些子程序。它还通过将类型 T 的详细信息放在包的 private 部分来演示封装。
T 的基本操作是函数 Create、重载过程 Work 和预定义的“=”运算符;Proc 不是基本程序,因为它使用 T 的访问类型作为参数——不要将此与访问参数混淆,如第二个过程 Work 中使用的那样。从 T 派生时,会继承基本操作。
with
P;package
Qis
type
Derivedis new
P.T;end
Q;
类型 Q.Derived 具有与 P.T 相同的数据以及相同行为;它继承了数据和子程序。因此,可以编写以下代码
with
Q;procedure
Mainis
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
Personis
type
Objectis
tagged
record
Name : String (1 .. 10); Gender : Gender_Type;end
record
;procedure
Put (O : Object);end
Person;
如您所见,Person.Object
在某种意义上是一个对象,因为它具有数据和行为(过程 Put
)。但是,此对象不会隐藏其数据;任何具有
子句的程序单元都可以直接读取和写入 Person.Object 中的数据。这破坏了封装,也说明了 Ada 完全将封装和类型的概念分开。以下是一个封装了其数据的 Person.Object 版本with
Person
package
Personis
type
Objectis
tagged private
;procedure
Put (O : Object);private
type
Objectis
tagged
record
Name : String (1 .. 10); Gender : Gender_Type;end
record
;end
Person;
因为类型 Person.Object 带有标签,所以可以创建记录扩展,它是一个具有额外数据的派生类型。
with
Person;package
Programmeris
type
Objectis
new
Person.Objectwith private
;private
type
Objectis
new
Person.Objectwith
record
Skilled_In : Language_List;end
record
;end
Programmer;
类型 Programmer.Object
继承了 Person.Object
的数据和行为,即类型的基本操作;因此可以编写
with
Programmer;procedure
Mainis
Me : Programmer.Object;begin
Programmer.Put (Me); Me.Put; -- equivalent to the above, Ada 2005 onlyend
Main;
因此,类型 Programmer.Object
作为 Person.Object
的记录扩展的声明,隐式声明了一个
,它适用于 procedure
PutProgrammer.Object
。
与无标签类型一样,Person 和 Programmer 类型的对象是可转换的。但是,在无标签对象可以双向转换的情况下,带标签类型的转换仅适用于根方向。(远离根的转换将必须凭空添加组件。)这种转换称为视图转换,因为组件不会丢失,它们只是变得不可见。
如果您要离开根,则必须使用扩展聚合。
现在我们已经引入了带标签类型、记录扩展和基本操作,就能够理解覆盖了。在上面的示例中,我们引入了一个名为 Person.Object
的类型,它有一个名为 Put
的基本操作。以下是包的主体
with
Ada.Text_IO;package body
Personis
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
Mainis
Me : Programmer.Object;begin
Programmer.Put (Me); Me.Put; -- equivalent to the above, Ada 2005 onlyend
Main;
那么程序将调用继承的基本操作 Put
,它将打印姓名和性别但不会打印额外数据。为了提供此额外行为,我们必须覆盖继承的过程 Put
,如下所示
with
Person;package
Programmeris
type
Objectis
new
Person.Objectwith private
;overriding
-- Optional keyword, new in Ada 2005procedure
Put (O : Object);private
type
Objectis
new
Person.Objectwith
record
Skilled_In : Language_List;end
record
;end
Programmer;
package body
Programmeris
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_Listend
Put;end
Programmer;
Programmer.Put
覆盖了 Person.Put
;换句话说,它完全替换了它。由于目的是扩展行为而不是替换行为,Programmer.Put
将 Person.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 laterbegin
Someone.Put; -- dynamic dispatchingend
;
Someone 的声明表示一个可能是任何类型的对象,Person.Object
或 Programmer.Object
。因此,对基本操作 Put
的调用将动态分派到 Person.Put
或 Programmer.Put
。
唯一的问题是,由于我们不知道 Someone 是否是程序员,所以我们也不知道 Someone 拥有多少个数据组件,因此我们也不知道 Someone 在内存中占用了多少字节。出于这个原因,类范围类型 Person.Object'Class
是不定的。在不指定任何约束的情况下,不可能声明此类型的对象。但是,可以
- 声明一个具有初始值的类范围对象(如上)。然后,该对象会受到其初始值的约束。
- 声明对该对象的访问值(因为访问值具有已知大小);
- 将类范围类型的对象作为参数传递给子程序
- 将特定类型的对象(特别是函数调用的结果)分配给类范围类型的变量。
有了这些知识,我们现在可以构建一个包含多个人的多态集合;在这个例子中,我们将简单地创建一个包含对人的访问值的数组
with
Person;procedure
Mainis
type
Person_Accessis access
Person.Object'Class;type
Array_Of_Personsis array
(Positiverange
<>) of Person_Access;function
Read_From_Diskreturn
Array_Of_Personsis separate
; Everyone :constant
Array_Of_Persons := Read_From_Disk;begin
-- Mainfor
Kin
Everyone'Rangeloop
Everyone (K).all
.Put; -- dereference followed by dynamic dispatchingend loop
;end
Main;
上面的过程实现了我们想要的目标:它遍历 Persons 数组,并调用适合每个人的过程 Put
。
您不需要了解动态分派的工作原理就能有效地使用它,但如果您好奇,以下是一个解释。
内存中每个对象的第一个组件是标签;这就是为什么对象是带标签类型而不是普通记录的原因。标签实际上是对表的访问值;每个特定类型都有一个表。表包含对该类型每个基本操作的访问值。在我们的示例中,由于存在两种类型 Person.Object
和 Programmer.Object
,因此存在两个表,每个表包含一个访问值。Person.Object
的表包含对 Person.Put
的访问值,而 Programmer.Object
的表包含对 Programmer.Put
的访问值。当您编译程序时,编译器会构建这两个表并将它们放在程序可执行代码中。
程序每次创建特定类型的新对象时,都会自动将其标签设置为指向相应的表。
程序每次执行基本操作的分派调用时,编译器都会插入以下对象代码
- 取消对标签的引用以查找当前对象特定类型的基本操作表
- 取消对基本操作访问值的引用
- 调用基本操作。
相反,当程序执行参数为对祖先类型的视图转换的调用时,编译器会在编译时而不是运行时执行这两个取消引用操作:此类调用是静态绑定的;编译器会发出直接调用视图转换中指定的祖先类型基本操作的代码。
分派由对象的(隐藏)标签控制。那么,当基本操作 Op1
对同一个对象调用另一个基本操作 Op2
时会发生什么呢?
type
Rootis
tagged
private
;procedure
Op1 (This: Root);procedure
Op2 (This: Root);type
Derivedis
new
Rootwith
private
; -- Derived inherits Op1overriding
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
Rootis
tagged
private
;procedure
Op1 (This: Root'Class);procedure
Op2 (This: Root);type
Derivedis
new
Rootwith
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
Baseis
tagged
private
;type
Derivedis
new
Basewith
private
;type
Leafis
new
Derivedwith
private
; ...procedure
Explicit_Dispatch (This :in
Base'Class)is
begin
if
Thisin
Leafthen
...end if
;if
Thisin
Derived'Classthen
...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
;
LRM 段落中提到的另一种创建对象的 方法是调用函数。将创建一个对象作为函数调用的返回值。因此,我们可以调用一个返回对象的函数,而不是使用初始值的聚合。
引入适当的 O-O 信息隐藏,我们更改包含Person
类型的包,以便Person
成为一个私有类型。为了使包的客户端能够构造Person
对象,我们声明一个返回它们的函数。(该函数可能会对对象执行一些有趣的构造工作。例如,上面的聚合很可能会根据提供的姓名字符串引发 Constraint_Error 异常;该函数可以对姓名进行混淆,使其与组件的声明相匹配。)我们还声明一个返回Person
对象姓名的函数。
package
Personsis
type
Personis
tagged
private
;function
Make (Name: String; Sex: Gender_Type)return
Person;function
Name (P: Person)return
String;private
type
Personis
tagged
record
Name : String (1 .. 10); Gender : Gender_Type;end
record
;end
Persons;
调用Make
函数会产生一个可用于初始化的对象。由于Person
类型是私有的,因此我们不能再引用P
的Name
组件。但是,有一个对应的函数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 > 1984then
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
Xis
type
Objectis
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_5return
Object;function
Primitive_6 (Everyone : Boolean)return
access
Object;end
X;
所有这些子程序都是基本操作。
基本操作还可以接受相同类型或其他类型的参数;此外,控制操作数不必是第一个参数
package
Xis
type
Objectis
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
Xis
type
Objectis
tagged
null
record
;type
Object_Accessis
access
Object;type
Object_Class_Accessis
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_4return
Object'Class;package
Inneris
procedure
Not_Primitive_5 (This :in
Object);end
Inner;end
X;
高级主题:冻结规则
[edit | edit source]冻结规则(ARM 13.14)可能是 Ada 语言定义中最复杂的部分;这是因为该标准试图尽可能明确地描述冻结。此外,语言定义的这一部分涉及所有实体的冻结,包括复杂的场景,如泛型和通过取消引用访问值访问的对象。但是,如果您了解动态分派的工作原理,您可以直观地了解标记类型的冻结。在那一节中,我们看到编译器为每个标记类型发出一个基本操作表。程序文本中发生此事件的点是标记类型冻结的点,即表变得完整的点。类型冻结后,就不能再向其中添加基本操作。
此点是以下最早的点:
- 声明标记类型的包规范的末尾
- 从标记类型派生的第一个类型的出现
示例
package
Xis
type
Objectis
tagged
null
record
;procedure
Primitive_1 (This:in
Object); -- this declaration freezes Objecttype
Derivedis
new
Objectwith
null
record
; -- illegal: declared after Object is frozenprocedure
Primitive_2 (This:in
Object);end
X;
直观地:在声明 Derived 的时候,编译器开始为派生类型创建一个新的基本操作表。最初,此新表等于父类型Object
的基本操作表。因此,Object
必须冻结。
- 标记类型变量的声明
示例
package
Xis
type
Objectis
tagged
null
record
;procedure
Primitive_1 (This:in
Object); V: Object; -- this declaration freezes Object -- illegal: Primitive operation declared after Object is frozenprocedure
Primitive_2 (This:in
Object);end
X;
直观地:在声明V
之后,就可以在V
上调用该类型的任何基本操作。因此,基本操作列表必须已知且完整,即冻结。
- 带标记类型的常量的完成(不是声明,如果有的话)
package
Xis
type
Objectis
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); -- OKprivate
-- only the completion freezes Object Deferred_Constant:constant
Object := (null
record
); -- illegal: declared after Object is frozenprocedure
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
Xis
type
Objectis
tagged
null
record
;function
Primitivereturn
access
Object; -- new in Ada 2005type
Derived_Objectis
new
Objectwith
null
record
;not overriding
-- new optional keywords in Ada 2005procedure
Primitive (This :in
Derived_Object); -- new primitive operationoverriding
function
Primitivereturn access
Derived_Object;end
X;
编译器将检查所需的行为。
这是一个良好的编程实践,因为它可以避免一些讨厌的错误,例如由于程序员拼写标识符错误,或者由于后来在父类型中添加了新的参数而导致没有覆盖继承的子程序。
它也可以用于抽象操作、重命名或实例化泛型子程序
not
overriding
procedure
Primitive_X (This :in
Object)is
abstract
;overriding
function
Primitive_Yreturn
Objectrenames
Some_Other_Subprogram;not
overriding
procedure
Primitive_Z (This :out
Object)is
new
Generic_Procedure (Element => Integer);
Object.Method 表示法
[edit | edit source]我们已经看到了这种表示法
package
Xis
type
Objectis
tagged
null
record
;procedure
Primitive (This:in
Object; That:in
Boolean);end
X;
with
X;procedure
Mainis
Obj : X.Object;begin
Obj.Primitive (That => True); -- Ada 2005 object.method notationend
Main;
这种表示法仅适用于控制参数是第一个参数的原始操作。
抽象类型
[edit | edit source]带标记类型也可以是抽象的(因此可以有抽象操作)
package
Xis
type
Objectis
abstract
tagged
…;procedure
One_Class_Member (This :in
Object);procedure
Another_Class_Member (This :in
out
Object);function
Abstract_Class_Memberreturn
Objectis
abstract
;end
X;
抽象操作不能有任何主体,因此派生类型被迫覆盖它(除非这些派生类型也是抽象的)。有关这方面的更多信息,请参阅下一节关于接口的内容。
与非抽象带标记类型不同的是,你不能声明任何此类型的变量。但是,你可以声明对它的访问,并将其用作类范围操作的参数。
通过接口实现多重继承
[edit | edit source]此语言特性仅从 Ada 2005 开始可用。
接口允许有限形式的多重继承(取自 Java)。从语义上讲,它们类似于“抽象带标记的空记录”,因为它们可能具有原始操作,但不能保存任何数据,因此这些操作不能有主体,它们要么被声明为abstract
或null
。抽象表示操作必须被覆盖,空表示默认实现为空主体,即不执行任何操作的实现。
接口用以下方式声明
package
Printableis
type
Objectis
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
Programmeris
type
Objectis
new
Person.Objectand
Printable.Objectwith
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
Derivedis
new
Parentand
Progenitor_1and
Progenitor_2 ...with
...;
通过 mix-in 实现多重继承
[edit | edit source]Ada 支持对接口的多重继承(见上文),但只支持对实现的单一继承。这意味着带标记类型可以实现多个接口,但只能扩展一个祖先带标记类型。
如果你想将行为添加到一个已经扩展了另一个类型的类型,这可能会成为问题;例如,假设你有
type
Baseis tagged private
;type
Derivedis new
Basewith private
;
并且你想使Derived
受控,即添加Derived
控制其初始化、赋值和终结的行为。可惜你不能写
type
Derivedis new
Baseand
Ada.Finalization.Controlledwith private
; -- illegal
因为Ada.Finalization
由于历史原因,没有定义接口Controlled
和Limited_Controlled
,而是定义了抽象类型。
如果你的基本类型不是受限的,那么没有很好的解决方案;你必须回到类的根部并使它受控。(原因将在稍后变得很明显。)
但是,对于受限类型,另一种解决方案是使用 mix-in
type
Baseis tagged limited private
;type
Derived;type
Controlled_Mix_In (Enclosing:access
Derived)is
new
Ada.Finalization.Limited_Controlledwith null record
;overriding procedure
Initialize (This:in out
Controlled_Mix_In);overriding procedure
Finalize (This:in out
Controlled_Mix_In);type
Derivedis new
Basewith 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: Derivedrenames
This.Enclosing.all
;begin
-- initialize Enclosing...end
Initialize;
以及Finalize
的类似情况。
这对于非受限类型不起作用的原因是通过判别式的自引用。想象一下,你拥有两个这样的非受限类型的变量,并将一个赋值给另一个
X := Y;
在赋值语句中,Adjust
只在X
的Finalize
之后被调用,因此不能提供判别式的新的值。因此X.Mixin_In.Enclosing
将不可避免地引用Y
。
现在让我们进一步扩展我们的层次结构
type
Furtheris new
Derivedwith null record
;overriding procedure
Initialize (This:in out
Further);overriding procedure
Finalize (This:in out
Further);
哎呀,这不起作用,因为还没有Derived
的相应过程 - 因此让我们快速添加它们。
type
Baseis tagged limited private
;type
Derived;type
Controlled_Mix_In (Enclosing:access
Derived)is
new
Ada.Finalization.Limited_Controlledwith null record
;overriding procedure
Initialize (This:in out
Controlled_Mix_In);overriding procedure
Finalize (This:in out
Controlled_Mix_In);type
Derivedis new
Basewith 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 newnot overriding procedure
Finalize (This:in out
Derived);type
Furtheris new
Derivedwith 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: Derivedrenames
This.Enclosing.all
;begin
Initialize (Enclosing);end
Initialize;
令我们沮丧的是,我们必须了解到,类型为Further
的对象的Initialize/Finalize
不会被调用,而是会调用其父类Derived
的Initialize/Finalize
。为什么?
declare
X: Further; -- Initialize (Derived (X)) is called herebegin
null
;end
; -- Finalize (Derived (X)) is called here
原因是 mix-in 在上面的重命名语句中定义了局部对象Enclosing
的类型为Derived
。为了解决这个问题,我们必须使用令人讨厌的重新分派(以不同的但等效的表示法显示)
overriding procedure
Initialize (This:in out
Controlled_Mix_In)is
Enclosing: Derivedrenames
This.Enclosing.all
;begin
Initialize (Derived'Class (Enclosing));end
Initialize;
overriding procedure
Finalize (This:in out
Controlled_Mix_In)is
Enclosing: Derived'Classrenames
Derived'Class (This.Enclosing.all
);begin
Enclosing.Finalize;end
Finalize;
declare
X: Further; -- Initialize (X) is called herebegin
null
;end
; -- Finalize (X) is called here
或者(可能更好)写
type
Controlled_Mix_In (Enclosing:access
Derived'Class)is
new
Ada.Finalization.Limited_Controlledwith null record
;
然后我们自动获得重新分派,并且可以省略Enclosing
上的类型转换。
类名
[edit | edit source]类包和类记录都需要一个名称。理论上它们可以有相同的名称,但实际上,当使用
子句时,这会导致讨厌的(由于直观错误消息)名称冲突。因此,随着时间的推移,三种事实上的命名标准已被广泛使用。use
类/类
[edit | edit source]包以复数名词命名,记录以相应的单数形式命名。
package
Personsis
type
Personis
tagged
record
Name : String (1 .. 10); Gender : Gender_Type;end
record
;end
Persons;
此约定是 Ada 内置库中通常使用的约定。
缺点:一些“复数”很难拼写,尤其是对于那些不是以英语为母语的人来说。
类/对象
[edit | edit source]包以类命名,记录只命名为 Object。
package
Personis
type
Objectis
tagged
record
Name : String (1 .. 10); Gender : Gender_Type;end
record
;end
Person;
缺点:你不能在任何时候对多个这样的类包使用
子句。但是,你始终可以使用“类型”而不是包。use
类/类_类型
[edit | edit source]包以类命名,记录以_Type 为后缀。
package
Personis
type
Person_Typeis
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
Pis
type
Cis tagged null record
;procedure
V (This :in out
C); -- primitive operation, will be inherited upon derivationprocedure
W (This :in out
C'Class); -- not primitive, will not be inherited upon derivationprocedure
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 boundend
;
动态分派
[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 boundend
;
如您所见,不需要访问类型或指针来在 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_Accessis access
C;
- C++ 不提供声明等效于以下内容的方法
type
C_Specific_Access_Oneis access
C;type
C_Specific_Access_Twois 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
Pis
type
T istagged
private
;function
Makereturn
T; -- constructorfunction
To_T (From: Integer)return
T; -- another constructorprocedure
Make (This:out
T); -- not a constructorprivate
...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
Pis
type
Tis
new
Ada.Finalization.Controlledwith
private
;function
Makereturn
T; -- constructorprivate
type
Tis
...end
record
;overriding
procedure
Initialize (This:in
out
T);overriding
procedure
Adjust (This:in
out
T); -- copy constructorend
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 hereend
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
Pis
type
Tis
new
Ada.Finalization.Controlledwith
private
;function
Makereturn
T; -- constructorprivate
type
Tis
...end
record
;overriding
procedure
Finalize (This:in
out
T); -- destructorend
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
CPPis
type
Public_Partis
abstract
tagged
record
-- no objects of this type A: Integer;end
record
;procedure
Public_Proc (This:in
out
Public_Part);type
Complete_Typeis
new
Public_Partwith
private
; -- procedure Public_Proc (This: in out Complete_Type); -- inherited, implicitly definedprivate
-- visible for childrentype
Private_Part; -- declaration stubtype
Private_Part_Pointeris
access
Private_Part;type
Private_Componentis
new
Ada.Finalization.Controlledwith
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_Typeis
new
Public_Partwith
record
B: Integer; P: Private_Component; -- must be controlled to avoid storage leaksend
record
;not
overriding
procedure
Protected_Proc (This: Complete_Type);end
CPP;
私有部分仅定义为存根,其完成隐藏在主体中。为了使它成为完整类型的组件,我们必须使用指针,因为组件的大小仍然未知(指针的大小对编译器来说是已知的)。不幸的是,使用指针,我们会冒内存泄漏的风险,因此我们必须使私有组件受控。
为了进行一个小测试,这是主体,其中子程序主体通过识别打印提供。
with
Ada.Unchecked_Deallocation;with
Ada.Text_IO;package
body
CPPis
procedure
Public_Proc (This:in
out
Public_Part)is
-- primitivebegin
Ada.Text_IO.Put_Line ("Public_Proc" & Integer'Image (This.A));end
Public_Proc;type
Private_Partis
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 copyend
Adjust;overriding
procedure
Finalize (X:in
out
Private_Component)is
procedure
Freeis
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 primitivebegin
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.Childis
procedure
Do_It (X: Complete_Type); -- not primitiveend
CPP.Child;
子类可以查看父类的私有部分,因此可以查看受保护的过程
with
Ada.Text_IO;package
body
CPP.Childis
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_CPPis
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 不需要这种构造,因为它默认定义流输入和输出操作:可以重写 Input
、Output
、Read
和 Write
属性的默认实现(以 Write
为例)。重写必须在类型冻结之前发生,即(在本示例的情况下)在包规范中。
private
with
Ada.Streams; -- needed only in the private partpackage
Pis
type
Cis
tagged
private
;private
type
Cis
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'Writeuse
My_Write; -- override the default attributeend
P;
默认情况下,Write
属性会按照声明中给出的顺序将组件发送到流,即 A、B 然后 C,因此我们更改了顺序。
package
body
Pis
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 componentsend
My_Write;end
P;
现在 P.C'Write
调用包的重写版本。
with
Ada.Text_IO.Text_Streams;with
P;procedure
Mainis
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++ |
---|---|
包 | 类(作为封装单元) |
带标记类型 | 类(对象)(作为类型)( *不* 指针或引用,它们是类范围的) |
基本操作 | 虚成员函数 |
标记 | 指向虚表的指针 |
类(类型) | 一个类树,以基类为根,包括该基类的所有(递归)派生类 |
类范围类型 | - |
类范围操作 | 静态成员函数 |
访问特定带标记类型的值 | - |
访问类范围类型的值 | 指向类的指针或引用 |
- 3.8:记录类型 [注释]
- 3.9:带标记类型和类型扩展 [注释]
- 3.9.1:类型扩展 [注释]
- 3.9.2:带标记类型的派发操作 [注释]
- 3.9.3:抽象类型和子程序 [注释]
- 3.10:访问类型 [注释]
- 3.8:记录类型 [注释]
- 3.9:带标记类型和类型扩展 [注释]
- 3.9.1:类型扩展 [注释]
- 3.9.2:带标记类型的派发操作 [注释]
- 3.9.3:抽象类型和子程序 [注释]
- 3.9.4:接口类型 [注释]
- 3.10:访问类型 [注释]