第21课|面向对象编程(标记记录)

30 阅读21分钟

本课目标

完成本课后,你将能够:

  • 理解标记记录(tagged record)作为 Ada 面向对象的基础原理及其内存布局
  • 掌握单继承与扩展记录的声明方式,区分重写与隐藏
  • 理解 primitive operations 的判定规则与动态分派(多态)的实现机制
  • 学会使用类宽类型(class-wide type)实现运行时多态,并掌握其使用限制
  • 区分抽象类型与接口的适用场景,理解多重继承的实现方式
  • 应用面向对象设计模式编写可扩展、易维护的程序
  • 分析动态分派的性能开销及在实时系统中的适用性

一、为什么 Ada 需要面向对象

Ada 最初于 1983 年设计时,面向对象尚未成为主流。然而,随着软件工程的发展,封装、继承、多态等特性被证明能极大提升代码复用性和可维护性。因此,Ada 95 引入了完整的面向对象特性,包括标记记录、类宽类型、抽象类型、接口等。这些特性建立在已有的强类型和包机制之上,既保留了 Ada 的安全性,又提供了与现代语言(C++、Java)相当的建模能力。

1.1 面向对象的核心诉求

在传统的结构化编程中,数据和操作是分离的。例如,我们有一个 Shape 记录和一组子程序 AreaDrawMove。当添加新形状(如三角形)时,需要修改所有相关子程序,在它们的 case 语句中添加新的分支。这违反了开闭原则(对扩展开放,对修改关闭),导致代码脆弱且难以维护。

面向对象通过将操作与类型绑定(称为 primitive operations),并支持动态分派,使得添加新形状时只需定义新类型及其操作,无需修改现有代码。这大大降低了模块间的耦合度,提高了系统的可扩展性。

1.2 Ada 面向对象设计的独特之处

与 C++ 相比,Ada 的类(标记记录)与包紧密结合。一个包通常定义一个类型及其 primitive operations,这种设计强化了封装和信息隐藏:类型的内部表示放在包的私有部分,外部只能通过公开的子程序访问。与 Java 相比,Ada 的对象默认是值语义(复制语义),而不是引用语义。这意味着赋值操作会复制整个对象,避免了别名带来的意外副作用。如果需要引用语义,可以显式使用访问类型(指针)。

此外,Ada 的面向对象是可选的,并不要求一切皆对象。开发者可以根据需要混合使用过程式、模块化、泛型和面向对象范式,这在嵌入式实时系统中尤为重要,因为动态分派可能带来轻微的性能开销(两次间接寻址),但 Ada 允许开发者精确控制是否使用多态。

1.3 典型应用场景

  • 图形系统:不同形状(圆、矩形、多边形)共享相同的绘制接口,由运行时根据实际类型决定调用哪个 Draw 函数。
  • 设备驱动框架:不同硬件(串口、USB、网络)使用统一的 OpenReadWriteClose 接口,通过多态管理设备列表。
  • 仿真系统:不同实体(坦克、士兵、建筑)具有相同的 UpdateRender 行为,通过类宽容器统一调度。
  • 策略模式:算法可以运行时替换,如排序策略(快速排序、归并排序)作为对象传递,实现算法的灵活切换。
  • 用户界面组件:按钮、文本框、复选框等控件共享相同的 DrawHandle_Event 接口,通过多态事件分发。

二、标记记录基础

2.1 什么是标记记录

普通记录(record)只是数据的聚合,没有类型层次。标记记录(tagged record)在记录中添加了一个隐藏的“标签”(tag),用于标识该记录的实际类型,从而支持继承和动态分派。这个标签通常是一个指向分派表(dispatch table)的指针或索引,分派表中存储该类型所有 primitive operations 的地址。

内存布局对比(假设 32 位系统,Float 4 字节,指针 4 字节,对齐按自然边界):

  • 普通记录 Point(X, Y: Float):内存布局为 X(4 字节)、Y(4 字节),共 8 字节。
  • 标记记录 Shape(tag + Center + Color):tag(4 字节)、Center(8 字节)、Color(10 字节)。由于对齐,Color 后可能填充 2 字节,总大小约 24 字节。

这意味着标记记录比普通记录多占用一个指针大小(通常是 4 或 8 字节),因此大量小对象的场景需评估内存开销。但通常这种开销在可接受范围内,尤其是当对象本身较大时。

2.2 声明标记记录

使用 tagged 关键字声明标记记录。为了保持封装性,通常将标记记录定义为私有类型(private),只通过子程序访问其内部数据。包规范中列出公开操作,包的私有部分给出记录的具体定义。

package Geometry is
   type Shape is tagged private;
   function Area (S : Shape) return Float;
   procedure Draw (S : Shape);
   procedure Move (S : in out Shape; Dx, Dy : Float);
private
   type Point is record X, Y : Float; end record;
   type Shape is tagged record
      Center : Point;
      Color  : String (1 .. 10);
   end record;
end Geometry;

这种设计将内部表示隐藏,只暴露必要的操作,符合 Ada 的哲学。外部代码只能通过 AreaDrawMove 来操作 Shape 对象,无法直接访问 CenterColor,保证了数据的完整性。

2.3 为什么需要标记

标记记录的核心作用是运行时类型识别(RTTI)。当你有一个类宽引用(Shape'Class)时,编译器无法在编译时知道具体是 Circle 还是 Rectangle,但通过对象头部的 tag,可以在运行时找到正确的分派表,从而调用正确的 Area 函数。没有 tag,动态分派无法实现。


三、继承与扩展

3.1 派生新类型

使用 new 关键字从标记记录派生,并通过 with 添加新组件。派生类型继承父类型的所有组件和 primitive operations(即那些以父类型为参数的操作)。

type Circle is new Shape with record
   Radius : Float;
end record;

type Rectangle is new Shape with record
   Width, Height : Float;
end record;

派生类型可以添加任意数量的新组件,这些新组件在内存中位于父组件之后。这种单继承机制简单、高效,避免了多重继承带来的复杂性(如菱形继承问题)。如果确实需要多重契约,可以使用接口(见第五节)。

3.2 重写(overriding)操作

子类型可以重新定义父类型的 primitive operation,使用 overriding 关键字(Ada 2005+)明确标记。这有两个好处:编译器会检查父类确实有该操作(防止拼写错误),并且代码可读性增强,明确表达了“这是一个重写操作”。

overriding function Area (C : Circle) return Float is
begin
   return 3.14159 * C.Radius ** 2;
end Area;

overriding function Area (R : Rectangle) return Float is
begin
   return R.Width * R.Height;
end Area;

如果不写 overriding,代码依然有效,但可能无意中创建了一个新的 primitive operation(如果父类没有该操作),容易出错。因此工业编码规范(如 JPL、High Integrity Ada)通常要求显式使用 overriding

3.3 调用父类操作

在重写的操作中,有时需要调用父类的实现。例如,CircleDraw 可能先画一个通用的形状边框,再画圆的特定部分。可以通过类型转换将派生对象视为父类对象,从而调用父类的操作:

overriding procedure Draw (C : Circle) is
begin
   Shape(C).Draw;  -- 先调用父类的 Draw
   -- 然后画圆的特定部分
   Ada.Text_IO.Put_Line ("... with radius" & Float'Image (C.Radius));
end Draw;

注意:Shape(C)Circle 视图转换为 Shape,这不会改变对象的内存布局,只是改变了编译器的类型视角。调用 Draw 时,由于 C 被当作 Shape,将调用 ShapeDraw(静态分派)。如果需要动态分派,应使用类宽类型。

3.4 扩展记录聚集

创建派生对象时,可以使用扩展记录聚集语法,先初始化父类部分,再用 with 初始化新组件。这种语法保证了父类部分被正确初始化,且可读性较好。

C : Circle := (Shape'(Center => (0.0, 0.0), Color => "Red      ") with Radius => 5.0);

如果不使用扩展聚集,也可以先赋值父类部分再单独赋值新组件,但扩展聚集更简洁。


四、Primitive Operations 与动态分派

4.1 什么是 primitive operation

Primitive operation 是在同一包中声明的、以该类型作为第一个参数(或返回类型)的子程序。这些操作会被派生类型继承,并且可以被重写,从而实现动态分派。判断规则(简化版):

  • 子程序的一个参数或返回值的类型是 Taccess T(其中 T 是标记记录类型)。
  • 子程序在声明 T 的包内(与 T 在同一包规范中)。
  • 子程序不是类宽类型参数(T'Class)——类宽类型用于多态接收,但本身不定义 primitive operation。
package Shapes is
   type Shape is tagged null record;
   -- 以下都是 Shape 的 primitive operations
   function Area (S : Shape) return Float;      -- 参数为 Shape
   procedure Draw (S : Shape);                  -- 参数为 Shape
   function Create return Shape;                -- 返回类型为 Shape
   procedure Reset (S : out Shape);             -- 参数为 out Shape
   procedure Transform (S : in out Shape);      -- 参数为 in out Shape
end Shapes;

如果一个子程序只接受 T'Class 参数,它不是 primitive operation,而是一个类宽操作,通常用于实现多态算法(如遍历容器)。类宽操作不会被继承,也不参与动态分派。

4.2 动态分派(多态)

当通过类宽类型'Class)的引用调用 primitive operation 时,Ada 会在运行时根据对象实际标签选择正确的实现。这是面向对象多态的核心机制。

procedure Process (S : in out Shape'Class) is
begin
   -- 运行时根据 S 的实际类型(Circle 或 Rectangle)调用对应的 Area
   Ada.Text_IO.Put_Line ("Area = " & Float'Image (S.Area));
end Process;

动态分派的实现步骤

  1. 从对象的 tag 字段获取分派表的地址。
  2. 在分派表中查找 Area 函数的地址(分派表是一个包含所有 primitive operation 指针的数组,每个操作有固定偏移)。
  3. 调用该函数,并传递对象的地址作为隐含参数(类似 this)。

这个过程通常需要两次间接寻址(一次取分派表,一次取函数地址),比直接调用稍慢(可能多几十个时钟周期),但在大多数应用中可接受。对于硬实时系统,需要评估最坏情况执行时间,但通常仍能满足要求。

4.3 类宽类型(class-wide type)

T'Class 表示类型 T 及其所有派生类型的集合。它是不确定的,因为派生类型可以添加新组件,导致大小不同。因此,类宽类型只能用于以下场景:

  • 子程序参数(in, in out, out)——通过引用传递(实际是传递对象的地址)。
  • 访问类型的目标(access T'Class)——通过指针间接访问。
  • 作为记录组件时,必须使用访问类型。
-- 合法:子程序参数
procedure Handle (S : Shape'Class);

-- 合法:访问类型
type Shape_Ptr is access all Shape'Class;

-- 合法:记录组件(使用访问类型)
type Holder is record
   S : access Shape'Class;
end record;

-- 非法:变量声明
-- S : Shape'Class;   -- 编译错误,类宽类型不能用于对象声明

类宽类型是实现多态的关键:它保存对象的完整信息,调用 primitive operation 时动态分派。注意,类宽类型的变量本身不是对象,而是一个引用视图,通常通过 'Class 属性获得,例如将具体对象赋值给类宽变量时,会发生视图转换(view conversion),不复制对象,只改变类型视角。


五、抽象类型与接口

5.1 抽象类型

使用 abstract 关键字定义抽象类型,不能直接实例化,只能作为基类。抽象类型通常包含抽象操作(没有实现),必须在派生类型中重写。

type Shape is abstract tagged null record;
function Area (S : Shape) return Float is abstract;  -- 抽象操作
procedure Draw (S : Shape) is abstract;

抽象类型可以有非抽象操作(有默认实现)。例如,MoveTo_String 可以有一个通用的实现,不需要每个派生类重写。派生类型必须重写所有抽象操作,否则自身也是抽象的,不能实例化。

5.2 接口(Ada 2005+)

接口是只有抽象操作的抽象类型,用于实现多重继承(多接口)。一个类型可以继承一个父类并实现多个接口。接口本身没有数据组件(null record),所有操作都是抽象的。

type Drawable is interface;
procedure Draw (D : Drawable) is abstract;

type Serializable is interface;
procedure Serialize (S : Serializable; Stream : access Root_Stream_Type'Class) is abstract;

-- 实现多个接口
type Circle is new Shape and Drawable and Serializable with record
   Radius : Float;
end record;

overriding procedure Draw (C : Circle) is ...;
overriding procedure Serialize (C : Circle; Stream : ...) is ...;

接口与抽象类型的区别:

  • 接口没有数据组件(null record),抽象类型可以有数据。
  • 一个类型只能继承一个父类(单继承),但可以实现多个接口。
  • 接口的所有操作都是抽象的(默认),抽象类型可以有非抽象操作。
  • 接口可以相互继承(type A is interface; type B is interface and A;)。

接口为 Ada 提供了类似 Java 的多重继承能力,同时避免了菱形继承问题(因为接口没有数据,不会产生冲突)。

5.3 何时使用抽象类型 vs 接口

  • 如果需要共享数据成员(如 CenterColor),使用抽象类型。
  • 如果只需要定义行为契约(如 DrawableComparable),使用接口。
  • 如果一个类型需要从多个来源继承行为,使用一个父类(抽象类型)加多个接口。

六、构造与销毁

6.1 构造函数

Ada 没有专门的构造函数,但可以使用函数返回初始化的对象,或使用记录聚集。这符合 Ada 的风格:对象的初始化通过显式赋值或聚集完成,不需要隐式调用构造函数。

function Make_Circle (X, Y, R : Float; Color : String) return Circle is
begin
   return Circle'(Center => (X, Y), Color => Color, Radius => R);
end Make_Circle;

使用工厂函数的好处是可以进行参数验证和复杂初始化。对于派生类型,工厂函数通常返回基类的类宽类型,以便隐藏具体类型。

6.2 控制初始化与终结

对于需要资源管理(如文件、锁、动态内存)的对象,可以使用 Ada.Finalization.Controlled 类型,它提供 InitializeAdjust(用于复制)和 Finalize(用于销毁)过程。这类似于 C++ 的构造函数、拷贝赋值和析构函数,或 Python 的 __init____del__

with Ada.Finalization;
type Resource is new Ada.Finalization.Controlled with record
   Handle : Integer;
end record;

procedure Initialize (R : in out Resource);
procedure Adjust (R : in out Resource);
procedure Finalize (R : in out Resource);

Resource 对象创建时(通过声明或 new),Initialize 被自动调用。当对象被赋值时,先调用 Adjust 处理复制。当对象离开作用域或被释放时,Finalize 被调用。这为资源管理提供了可靠的基础设施。该特性将在后续“资源管理”课程中深入讲解。


七、完整示例:图形系统

-- 文件名:shapes.ads
-- 目的:定义图形系统的抽象基类和具体形状

with Ada.Text_IO;

package Shapes is
   -- 辅助类型:点
   type Point is record
      X, Y : Float;
   end record;
   
   -- 抽象基类:Shape
   -- 所有形状共享位置(Center)和颜色(简化,这里省略颜色)
   -- 使用 abstract 标记,表示不能直接实例化
   type Shape is abstract tagged record
      Center : Point;
   end record;
   
   -- 抽象操作:面积(必须由派生类实现)
   function Area (S : Shape) return Float is abstract;
   -- 抽象操作:绘制(必须由派生类实现)
   procedure Draw (S : Shape) is abstract;
   
   -- 非抽象操作:移动(所有形状通用,无需重写)
   -- 实现见包体
   procedure Move (S : in out Shape; Dx, Dy : Float);
   -- 非抽象操作:转换为字符串(通用,但可被重写)
   -- 这里不是 abstract,所以派生类可以选择重写或不重写
   function To_String (S : Shape) return String;
   
   -- 具体派生类型:圆
   type Circle is new Shape with record
      Radius : Float;
   end record;
   
   -- 重写抽象操作,使用 overriding 关键字
   overriding function Area (C : Circle) return Float;
   overriding procedure Draw (C : Circle);
   -- 可选:重写 To_String
   overriding function To_String (C : Circle) return String;
   
   -- 具体派生类型:矩形
   type Rectangle is new Shape with record
      Width, Height : Float;
   end record;
   
   overriding function Area (R : Rectangle) return Float;
   overriding procedure Draw (R : Rectangle);
   
   -- 多态处理过程:接受任何 Shape 派生类型
   -- 此处使用类宽类型参数实现动态分派
   procedure Display (S : Shape'Class);
   
   -- 接口示例:可序列化(Ada 2005+)
   type Serializable is interface;
   procedure Serialize (S : Serializable; Stream : access Ada.Text_IO.File_Type'Class) is abstract;
   
   -- 让 Circle 实现 Serializable 接口
   -- 注意:需要定义一个新类型,因为接口实现不能直接在原类型上添加
   type Serializable_Circle is new Circle and Serializable with null record;
   overriding procedure Serialize (SC : Serializable_Circle; Stream : access Ada.Text_IO.File_Type'Class);
   
end Shapes;
-- 文件名:shapes.adb
-- 实现 Shapes 包

package body Shapes is
   
   -- 实现通用移动
   procedure Move (S : in out Shape; Dx, Dy : Float) is
   begin
      S.Center.X := S.Center.X + Dx;
      S.Center.Y := S.Center.Y + Dy;
   end Move;
   
   -- 默认的 To_String 实现
   function To_String (S : Shape) return String is
   begin
      return "Shape at (" & Float'Image (S.Center.X) & ", " &
             Float'Image (S.Center.Y) & ")";
   end To_String;
   
   -- Circle 实现
   function Area (C : Circle) return Float is
   begin
      return 3.14159 * C.Radius * C.Radius;
   end Area;
   
   procedure Draw (C : Circle) is
   begin
      Ada.Text_IO.Put_Line ("Circle at (" & Float'Image (C.Center.X) & ", " &
                            Float'Image (C.Center.Y) & ") radius=" &
                            Float'Image (C.Radius));
   end Draw;
   
   -- Circle 重写 To_String
   function To_String (C : Circle) return String is
   begin
      return "Circle at (" & Float'Image (C.Center.X) & ", " &
             Float'Image (C.Center.Y) & ") radius=" & Float'Image (C.Radius);
   end To_String;
   
   -- Rectangle 实现
   function Area (R : Rectangle) return Float is
   begin
      return R.Width * R.Height;
   end Area;
   
   procedure Draw (R : Rectangle) is
   begin
      Ada.Text_IO.Put_Line ("Rectangle at (" & Float'Image (R.Center.X) & ", " &
                            Float'Image (R.Center.Y) & ") " &
                            Float'Image (R.Width) & "x" & Float'Image (R.Height));
   end Draw;
   
   -- 多态过程:通过类宽类型参数实现动态分派
   procedure Display (S : Shape'Class) is
   begin
      Draw (S);  -- 动态分派:根据 S 的实际类型调用对应的 Draw
      Ada.Text_IO.Put_Line ("Area = " & Float'Image (Area (S)));
   end Display;
   
   -- 实现 Serializable 接口
   overriding procedure Serialize (SC : Serializable_Circle; Stream : access Ada.Text_IO.File_Type'Class) is
   begin
      Ada.Text_IO.Put (Stream.all, "Circle,");
      Ada.Text_IO.Put (Stream.all, Float'Image (SC.Center.X) & ",");
      Ada.Text_IO.Put (Stream.all, Float'Image (SC.Center.Y) & ",");
      Ada.Text_IO.Put_Line (Stream.all, Float'Image (SC.Radius));
   end Serialize;
   
end Shapes;
-- 文件名:main.adb
-- 测试面向对象特性

with Shapes; use Shapes;
with Ada.Text_IO;

procedure Main is
   C : Circle := (Center => (0.0, 0.0), Radius => 5.0);
   R : Rectangle := (Center => (10.0, 10.0), Width => 4.0, Height => 3.0);
   
   -- 类宽类型数组:存储不同形状的访问值
   type Shape_Ptr is access all Shape'Class;
   Shapes_Array : array (1 .. 2) of Shape_Ptr := (new Circle'(C), new Rectangle'(R));
   
   -- 接口测试
   SC : Serializable_Circle := (Center => (1.0, 2.0), Radius => 3.0);
   File : Ada.Text_IO.File_Type;
begin
   -- 直接调用(静态分派)
   Ada.Text_IO.Put_Line("=== Direct calls ===");
   Draw (C);
   Draw (R);
   
   -- 多态调用(动态分派)
   Ada.Text_IO.New_Line;
   Ada.Text_IO.Put_Line("=== Polymorphic calls ===");
   Display (C);
   Display (R);
   
   -- 通过类宽引用数组统一处理
   Ada.Text_IO.New_Line;
   Ada.Text_IO.Put_Line("=== Array of class-wide ===");
   for I in Shapes_Array'Range loop
      Display (Shapes_Array (I).all);
   end loop;
   
   -- 接口使用:序列化到文件
   Ada.Text_IO.Create (File, Ada.Text_IO.Out_File, "circle.csv");
   Serialize (SC, File'Access);
   Ada.Text_IO.Close (File);
   Ada.Text_IO.Put_Line ("Serialized circle to circle.csv");
end Main;

编译运行:

gnatmake main.adb
./main

输出示例:

=== Direct calls ===
Circle at ( 0.00000E+00,  0.00000E+00) radius= 5.00000E+00
Rectangle at ( 1.00000E+01,  1.00000E+01)  4.00000E+00x 3.00000E+00

=== Polymorphic calls ===
Circle at ( 0.00000E+00,  0.00000E+00) radius= 5.00000E+00
Area =  7.85398E+01
Rectangle at ( 1.00000E+01,  1.00000E+01)  4.00000E+00x 3.00000E+00
Area =  1.20000E+01

=== Array of class-wide ===
Circle at ( 0.00000E+00,  0.00000E+00) radius= 5.00000E+00
Area =  7.85398E+01
Rectangle at ( 1.00000E+01,  1.00000E+01)  4.00000E+00x 3.00000E+00
Area =  1.20000E+01
Serialized circle to circle.csv

八、本课总结

  • 标记记录tagged)是 Ada 面向对象的基础,通过隐藏标签支持运行时类型识别和动态分派。
  • 使用 new ... with 实现单继承,可以添加新组件和重写 primitive operations。
  • Primitive operations 是与类型绑定的子程序,支持重写和动态分派。判断规则:在同一包中,参数或返回值为该类型(非类宽)。
  • 动态分派通过类宽类型(T'Class)实现,在运行时根据对象实际标签选择操作。类宽类型只能用于参数、访问类型,不能声明变量。
  • 抽象类型abstract)定义接口契约,不能实例化;接口interface)允许多重继承,没有数据组件。
  • Ada 的面向对象与包机制紧密结合,强调封装和强类型安全。动态分派性能开销可控(两次间接寻址),适合实时系统。
  • 通过 Ada.Finalization.Controlled 可以实现资源管理的构造/析构机制。

九、课后练习

  1. 基本继承:定义 Square 类型继承自 Rectangle,重写 Area(确保面积计算正确)和 Draw(输出“正方形”)。测试其多态行为,将 Square 对象传递给 Display 过程,观察输出。

  2. 多态容器:实现一个 Shape_List 包,内部使用 access Shape'Class 的动态数组(或链表)。提供 Add_Shape(接受任何形状)、Display_All(遍历并调用 Draw)和 Total_Area(返回所有形状总面积)函数。测试添加不同形状。

  3. 抽象工厂:定义一个抽象类型 Button,包含抽象操作 Click。派生 WindowsButtonLinuxButton,实现不同的 Click 行为(输出不同消息)。编写多态测试,通过一个数组(Button'Class 访问类型)统一调用 Click。分析如果添加 MacButton 需要修改哪些代码。

  4. 接口实践:定义 Comparable 接口,包含 Less 抽象函数(返回 Boolean)。让 Circle 实现按面积比较,Rectangle 也实现按面积比较。编写函数 Max_Area,接受两个 Shape'Class,返回面积较大的那个(使用 Less 接口)。注意:需要将 Shape'Class 转换为 Comparable'Class 才能调用 Less

  5. 动态分派深入:分析如果没有类宽类型而使用具体类型,为何无法实现多态?编写代码对比:一个过程接受 Shape 而不是 Shape'Class,传递 Circle 会发生什么(静态分派)。写出你的分析,包括编译行为和运行时输出。

  6. 内存布局:使用 'Size'Address 属性,打印 ShapeCircleRectangle 对象的大小和组件偏移(提示:可使用 'Position'First_Bit'Last_Bit 对于记录组件)。解释为什么标记记录比普通记录大,并计算理论大小与实际大小的差异(考虑对齐)。

  7. 重写与隐藏:在 Circle 中定义一个与父类 Shape 同名的函数 To_String(参数为 Circle,是重写)。测试调用 Display(接受 Shape'Class)时,To_String 是否被动态分派?如果 To_String 不是 primitive operation(例如参数是 Circle'Class),会发生什么?解释原因。

  8. 访问类型与多态:使用 access Shape'Class 创建单向链表,每个节点可以指向任何形状。实现链表插入(头插法)、遍历并调用 Draw。注意内存释放:实现一个 Free_List 过程,遍历释放所有节点和形状对象。

  9. 异常与多态:在 CircleArea 函数中,如果半径为负数,抛出自定义异常 Negative_Radius(带错误消息)。在多态调用(通过 Display)中捕获该异常,并输出“无效圆半径”。确保异常不影响其他形状的处理。

  10. 设计模式:策略模式:实现“策略模式”。定义一个接口 Sort_Strategy,包含 Sort 抽象操作(接受 in out Integer_Array)。实现两个策略:Bubble_Sort_StrategyQuick_Sort_Strategy。编写一个 Context 类(标记记录),包含一个策略访问值(access Sort_Strategy'Class),提供 Set_StrategyExecute_Sort 方法。在主程序中,动态切换策略并排序同一数组,验证结果正确性。


十、下节预告

第22课|访问类型的高级特性

我们将:

  • 深入理解访问类型与动态分派的结合(access T'Class 的用途)
  • 掌握通用访问类型(access all)与匿名访问(access 参数)的区别和适用场景
  • 学习访问类型在容器(链表、树)和回调(函数指针)中的应用
  • 分析内存管理策略:手动释放(Unchecked_Deallocation)、存储池(Storage_Pool)、引用计数(Ada.Containers.Reference_Counted
  • 理解访问类型的安全性限制(aliasednot null 等)

关键术语表

标记记录(tagged record):包含隐藏标签的记录,支持继承和动态分派。标签指向分派表。

扩展记录(extension):在父记录基础上添加新组件的派生类型,语法 new T with ...

Primitive operation:与类型绑定的子程序,可被继承和重写,是实现多态的基础。判定规则:参数或返回值为该类型(非类宽),且在同一包中。

动态分派(dynamic dispatch):运行时根据对象实际类型选择 primitive operation 的机制,通过分派表实现。

类宽类型(class-wide type)T'Class,表示 T 及其派生类型的集合,用于多态参数。不能声明变量,只能用于参数和访问类型。

分派表(dispatch table):每个标记记录类型关联的表,存储所有 primitive operations 的地址。对象通过 tag 找到分派表。

抽象类型(abstract type):不能实例化的基类型,通常包含抽象操作。

抽象操作(abstract operation):没有实现的子程序,必须在派生类型中重写。

接口(interface):纯抽象类型,没有数据组件,允许多重继承(type T is interface)。

视图转换(view conversion):将派生对象视为父类对象或类宽对象的类型转换,不改变内存布局。