第13课|记录类型详解

66 阅读8分钟

本课目标

完成本课后,你将能够:

  • 掌握记录类型的声明与使用
  • 熟练运用记录聚集进行初始化
  • 理解记录变体(判别式)实现类型安全联合体
  • 学习记录的表示子句控制内存布局
  • 应用记录进行复杂数据建模

一、记录类型基础

1.1 什么是记录

记录是异构数据的聚合体,类似其他语言中的结构体(struct),可将不同类型的数据组合成一个逻辑单元。

-- 基本记录声明
type Point is record
   X : Float;
   Y : Float;
end record;

-- 使用记录
P : Point;
P.X := 10.5;
P.Y := 20.3;

1.2 记录组件

记录可以包含任意类型的组件,包括标量、数组,甚至其他记录:

type Date is record
   Year  : Integer range 1900 .. 2100;
   Month : Integer range 1 .. 12;
   Day   : Integer range 1 .. 31;
end record;

type Person is record
   Name    : String (1 .. 50);
   Age     : Integer range 0 .. 150;
   Birth   : Date;                    -- 嵌套记录
   Salary  : Float;
end record;

-- 访问嵌套组件
John : Person;
John.Birth.Year := 1990;

1.3 记录初始化

记录变量声明时可以同时初始化:

-- 位置关联(按声明顺序)
P : Point := (10.5, 20.3);

-- 命名关联(推荐,更清晰)
P : Point := (X => 10.5, Y => 20.3);

-- 部分初始化(其余用 others,但必须覆盖所有组件)
P : Point := (X => 10.5, others => 0.0);

-- 嵌套记录初始化
John : Person := (
   Name   => "John Doe                    ",
   Age    => 30,
   Birth  => (Year => 1990, Month => 5, Day => 15),
   Salary => 5000.0
);

二、记录属性与操作

2.1 记录属性

记录类型提供了有限但有用的属性:

属性含义示例
'Size记录占用的总位数Person'Size
'Alignment记录的对齐要求Person'Alignment
'Bit_Order位顺序(与表示子句相关)-

2.2 记录的赋值与比较

记录支持整体赋值和相等比较:

P1, P2 : Point;
P1 := (10.5, 20.3);
P2 := P1;                    -- 整体赋值,复制所有组件
if P1 = P2 then              -- 相等比较,要求所有组件相等
   Ada.Text_IO.Put_Line ("相同");
end if;

注意:如果记录包含无约束数组(如 String 类型未指定长度),则赋值和比较可能受限,但通常可以使用有约束组件。

2.3 记录作为子程序参数

记录可以作为子程序的参数,可以按 inin outout 传递:

procedure Move (P : in out Point; Delta_X, Delta_Y : Float) is
begin
   P.X := P.X + Delta_X;
   P.Y := P.Y + Delta_Y;
end Move;

function Distance (P1, P2 : Point) return Float is
begin
   return (P2.X - P1.X) ** 2 + (P2.Y - P1.Y) ** 2;
end Distance;

三、记录变体(判别式)

3.1 变体记录的概念

变体记录允许记录的结构根据判别式动态改变,是一种类型安全的联合体(union)。

type Shape_Kind is (Circle, Rectangle, Triangle);

type Shape (Kind : Shape_Kind) is record
   Color : String (1 .. 10);        -- 所有变体共享的组件
   case Kind is
      when Circle =>
         Radius : Float;
      when Rectangle =>
         Width, Height : Float;
      when Triangle =>
         Side_A, Side_B, Side_C : Float;
   end case;
end record;

3.2 变体记录的使用

声明变体记录变量时必须指定判别式:

C : Shape (Kind => Circle);      -- 判别式为 Circle
C.Radius := 5.0;
C.Color := "Red      ";

R : Shape (Kind => Rectangle);
R.Width := 10.0;
R.Height := 20.0;

-- 访问不存在的组件会编译错误
-- C.Width := 1.0;   -- 错误!Circle 变体没有 Width

3.3 判别式的约束与默认值

可以为判别式提供默认值:

type Shape (Kind : Shape_Kind := Circle) is record  -- 默认 Circle
   Color : String (1 .. 10);
   case Kind is
      when Circle => Radius : Float;
      when Rectangle => Width, Height : Float;
      when Triangle => Side_A, Side_B, Side_C : Float;
   end case;
end record;

-- 可以省略判别式,使用默认值
C : Shape;                     -- 默认 Kind = Circle
C := (Kind => Rectangle,      -- 可以改变判别式,但会导致组件变化
      Color => "Blue     ",
      Width => 10.0, Height => 20.0);

3.4 判别式改变与约束

Ada 规定,改变判别式时,旧的变体组件会被丢弃,新的变体组件被初始化。需要整体赋值:

C : Shape (Kind => Circle);
C.Radius := 5.0;
-- 要改成矩形,必须整体赋值
C := (Kind => Rectangle, Color => "Yellow   ", Width => 8.0, Height => 12.0);
-- 此时 C.Width 可用,C.Radius 不再存在

四、记录的表示子句

4.1 内存布局控制

在嵌入式或系统编程中,可能需要精确控制记录的内存布局,以匹配硬件寄存器或协议数据包。Ada 提供表示子句来实现:

-- 位字段定义
type Status_Register is record
   Enable   : Boolean;   -- 位 0
   Ready    : Boolean;   -- 位 1
   Error    : Boolean;   -- 位 2
   Reserved : Integer range 0 .. 2#11111#;  -- 位 3..7
end record;

-- 指定存储布局
for Status_Register use record
   Enable   at 0 range 0 .. 0;
   Ready    at 0 range 1 .. 1;
   Error    at 0 range 2 .. 2;
   Reserved at 0 range 3 .. 7;
end record;
for Status_Register'Size use 8;  -- 总大小 8 位

4.2 使用 at 子句指定地址

可以使用 at 子句将变量固定在特定内存地址,常用于硬件寄存器映射:

Hardware_Status : Status_Register;
for Hardware_Status'Address use System'To_Address (16#4000_1000#);
pragma Volatile (Hardware_Status);  -- 禁止优化,因硬件可能异步改变

4.3 紧凑存储

使用 pragma Pack 可以压缩记录,取消对齐填充:

type Packed_Record is record
   A : Boolean;
   B : Integer range 0 .. 255;
   C : Boolean;
end record;
pragma Pack (Packed_Record);
-- 现在大小可能从 12 字节压缩到 1+1+1=3 字节(取决于编译器)

五、记录与数组的配合

5.1 记录数组

可以声明记录类型的数组:

type Person_Array is array (1 .. 100) of Person;
Employees : Person_Array;

-- 初始化
Employees (1) := (Name => "Alice     ", Age => 25, 
                  Birth => (1998, 3, 12), Salary => 4500.0);

5.2 数组作为记录组件

type Matrix is array (1 .. 3, 1 .. 3) of Float;
type Data_Record is record
   Name : String (1 .. 20);
   M    : Matrix;
end record;

六、记录的继承(标记记录简介)

Ada 支持面向对象编程,标记记录(tagged record)是基类的基础。本课简要介绍,详细内容将在后续面向对象章节展开。

-- 标记记录(tagged record)
type Shape is tagged record
   Color : String (1 .. 10);
end record;

-- 从 Shape 派生
type Circle is new Shape with record
   Radius : Float;
end record;

-- 可重载操作
function Area (S : Shape) return Float is (0.0);
function Area (C : Circle) return Float is (3.14159 * C.Radius ** 2);

七、完整示例:学生信息管理系统

-- 文件名: student_record.adb
-- 功能:使用记录管理学生信息

with Ada.Text_IO;
with Ada.Integer_Text_IO;
with Ada.Float_Text_IO;

procedure Student_Record is

   -- 学生类型枚举
   type Student_Type is (Undergrad, Graduate);

   -- 学生记录(带变体)
   type Student (Category : Student_Type := Undergrad) is record
      ID     : Integer range 10000 .. 99999;
      Name   : String (1 .. 20);
      Age    : Integer range 18 .. 60;
      GPA    : Float range 0.0 .. 4.0;
      case Category is
         when Undergrad =>
            Year : Integer range 1 .. 4;      -- 年级
         when Graduate =>
            Advisor : String (1 .. 20);       -- 导师姓名
            Thesis_Title : String (1 .. 50);
      end case;
   end record;

   -- 数据库:最多100名学生
   type Student_Array is array (1 .. 100) of Student;
   Students : Student_Array;
   Student_Count : Integer := 0;

   -- 打印学生信息
   procedure Print_Student (S : Student) is
   begin
      Ada.Text_IO.Put ("ID: " & Integer'Image (S.ID));
      Ada.Text_IO.Put (", Name: " & S.Name);
      Ada.Text_IO.Put (", Age: " & Integer'Image (S.Age));
      Ada.Text_IO.Put (", GPA: ");
      Ada.Float_Text_IO.Put (S.GPA, Fore => 1, Aft => 2, Exp => 0);
      
      case S.Category is
         when Undergrad =>
            Ada.Text_IO.Put_Line (", Year: " & Integer'Image (S.Year));
         when Graduate =>
            Ada.Text_IO.Put (", Advisor: " & S.Advisor);
            Ada.Text_IO.Put_Line (", Thesis: " & S.Thesis_Title);
      end case;
   end Print_Student;

   -- 添加本科生
   procedure Add_Undergrad (ID : Integer; Name : String; Age : Integer; 
                            GPA : Float; Year : Integer) is
   begin
      Student_Count := Student_Count + 1;
      Students (Student_Count) := (Category => Undergrad,
                                   ID => ID,
                                   Name => Name,
                                   Age => Age,
                                   GPA => GPA,
                                   Year => Year);
   end Add_Undergrad;

   -- 添加研究生
   procedure Add_Graduate (ID : Integer; Name : String; Age : Integer;
                           GPA : Float; Advisor : String; 
                           Thesis_Title : String) is
   begin
      Student_Count := Student_Count + 1;
      Students (Student_Count) := (Category => Graduate,
                                   ID => ID,
                                   Name => Name,
                                   Age => Age,
                                   GPA => GPA,
                                   Advisor => Advisor,
                                   Thesis_Title => Thesis_Title);
   end Add_Graduate;

   -- 查找最高 GPA 的学生
   function Find_Top_Student return Integer is
      Top_Index : Integer := 1;
   begin
      for I in 2 .. Student_Count loop
         if Students (I).GPA > Students (Top_Index).GPA then
            Top_Index := I;
         end if;
      end loop;
      return Top_Index;
   end Find_Top_Student;

begin
   -- 添加一些示例数据
   Add_Undergrad (10001, "Alice                ", 20, 3.8, 3);
   Add_Undergrad (10002, "Bob                  ", 21, 3.5, 4);
   Add_Graduate (20001, "Charlie              ", 25, 3.9, 
                 "Dr. Smith            ", "Deep Learning in Ada");
   Add_Graduate (20002, "Diana                ", 24, 3.7,
                 "Dr. Johnson          ", "Real-Time Systems");

   -- 打印所有学生
   Ada.Text_IO.Put_Line ("=== 学生列表 ===");
   for I in 1 .. Student_Count loop
      Ada.Text_IO.Put (Integer'Image (I) & ". ");
      Print_Student (Students (I));
   end loop;

   -- 输出最高 GPA 学生
   declare
      Top := Find_Top_Student;
   begin
      Ada.Text_IO.New_Line;
      Ada.Text_IO.Put_Line ("=== 最高 GPA 学生 ===");
      Print_Student (Students (Top));
   end;

end Student_Record;

编译运行:

gnatmake student_record.adb
./student_record

八、本课总结

  • 记录是异构数据的聚合,通过 record ... end record 定义
  • 记录聚集支持位置关联和命名关联初始化
  • 变体记录使用判别式实现类型安全的联合体,结构可根据判别式动态变化
  • 表示子句(at 子句、for ... use record)可精确控制内存布局,用于硬件映射和协议数据
  • pragma Pack 压缩记录以节省空间
  • 记录可作为数组元素,也可包含数组
  • 标记记录(tagged record)是 Ada 面向对象的基础

九、课后练习

  1. 基本记录:定义一个 Complex 记录(实部、虚部),编写函数实现复数加法、减法和乘法。

  2. 变体记录:定义一个 Vehicle 记录,根据 Vehicle_Type(Car、Truck、Motorcycle)包含不同的字段(如 Car 有 num_doors,Truck 有 cargo_capacity 等),并编写程序输出不同车辆的信息。

  3. 表示子句:定义一个 Control_Register 记录,包含 3 个布尔字段和 1 个 5 位整数字段,使用表示子句精确控制位布局,并尝试输出其大小。

  4. 记录数组:创建一个包含 10 个 Point 记录的数组,计算所有点的中心点(X、Y 的平均值)。

  5. 学生记录扩展:在学生管理系统中增加一个函数,按 GPA 排序并输出前三名学生。


十、下节预告

第14课|访问类型(指针)

我们将:

  • 理解 Ada 访问类型与普通指针的区别
  • 掌握访问类型的声明与使用
  • 学习动态内存分配与释放
  • 理解访问类型的安全性机制(作用域、null 排除)
  • 应用访问类型构建链表、树等动态数据结构

关键术语表

记录:异构数据的聚合体,类似其他语言的 struct

组件:记录中的单个数据字段

记录聚集:一次性初始化记录所有组件的语法

判别式:变体记录中决定当前变体结构的字段

变体记录:根据判别式改变组件集合的记录类型

表示子句:指定记录或组件存储布局的语法(atfor ... use

pragma Pack:编译器指令,要求紧凑存储记录,取消对齐填充

标记记录:支持继承和多态的记录类型,Ada 面向对象的基础

位字段:在单个存储单元中打包多个小范围数据的布局技术