第16课|包与模块化设计

11 阅读8分钟

本课目标

完成本课后,你将能够:

  • 理解包(package)作为模块化单元的核心作用
  • 掌握包规范与包体的分离设计
  • 熟练使用私有类型实现信息隐藏
  • 理解包的初始化与子程序可见性规则
  • 应用包构建可重用、可维护的大型程序

一、包的基本概念

1.1 什么是包

包是 Ada 中最重要的模块化构造,它将相关的类型、常量、变量、子程序组织在一起,形成一个逻辑单元。包通常分为两部分:

  • 包规范(package specification):公开接口,声明对外可见的类型、子程序等
  • 包体(package body):私有实现,定义子程序的具体实现和内部细节
-- 包规范(文件:math_utils.ads)
package Math_Utils is
   function Square (X : Float) return Float;
   function Cube (X : Float) return Float;
end Math_Utils;

-- 包体(文件:math_utils.adb)
package body Math_Utils is
   function Square (X : Float) return Float is
   begin
      return X * X;
   end Square;
   
   function Cube (X : Float) return Float is
   begin
      return X * X * X;
   end Cube;
end Math_Utils;

1.2 包的使用

通过 with 子句引入包,然后使用点号访问包中的声明:

with Ada.Text_IO;
with Math_Utils;

procedure Main is
   Result : Float;
begin
   Result := Math_Utils.Square (5.0);
   Ada.Text_IO.Put_Line (Float'Image (Result));
end Main;

1.3 use 子句

使用 use 可以免去包名前缀,但可能引起命名冲突:

with Math_Utils;
use Math_Utils;

procedure Main is
begin
   -- 可以直接调用 Square,无需 Math_Utils. 前缀
   Ada.Text_IO.Put_Line (Float'Image (Square (5.0)));
end Main;

工业建议:在包规范中避免 use,在包体中可以酌情使用。


二、包规范与包体

2.1 包规范(可见部分)

包规范定义了包的公共接口,包括:

  • 公开类型声明
  • 常量
  • 子程序声明(无实现)
  • 异常声明
package Stack is
   type Stack_Type is private;      -- 私有类型,外部无法访问内部结构
   procedure Push (S : in out Stack_Type; Item : Integer);
   procedure Pop (S : in out Stack_Type; Item : out Integer);
   function Is_Empty (S : Stack_Type) return Boolean;
   function Is_Full (S : Stack_Type) return Boolean;
   Stack_Error : exception;
private
   type Stack_Array is array (1 .. 100) of Integer;
   type Stack_Type is record
      Data : Stack_Array;
      Top  : Integer range 0 .. 100 := 0;
   end record;
end Stack;

2.2 包体(私有实现)

包体实现包规范中声明的子程序,也可以包含私有的类型、子程序等:

package body Stack is
   procedure Push (S : in out Stack_Type; Item : Integer) is
   begin
      if Is_Full (S) then
         raise Stack_Error;
      end if;
      S.Top := S.Top + 1;
      S.Data (S.Top) := Item;
   end Push;
   
   procedure Pop (S : in out Stack_Type; Item : out Integer) is
   begin
      if Is_Empty (S) then
         raise Stack_Error;
      end if;
      Item := S.Data (S.Top);
      S.Top := S.Top - 1;
   end Pop;
   
   function Is_Empty (S : Stack_Type) return Boolean is
   begin
      return S.Top = 0;
   end Is_Empty;
   
   function Is_Full (S : Stack_Type) return Boolean is
   begin
      return S.Top = S.Data'Last;
   end Is_Full;
end Stack;

2.3 分离编译

包规范和包体通常放在不同文件中:

  • 规范文件扩展名:.ads(Ada Specification)
  • 体文件扩展名:.adb(Ada Body)

编译时,gnatmake 自动处理依赖关系:

gnatmake main.adb   # 自动编译所需的包

三、私有类型与信息隐藏

3.1 私有类型

私有类型(private)在包规范中声明,但完整定义放在 private 部分之后。外部代码只能使用该类型对象和公开子程序,无法访问内部结构。

package Bank_Account is
   type Account is private;
   procedure Deposit (A : in out Account; Amount : Float);
   procedure Withdraw (A : in out Account; Amount : Float);
   function Balance (A : Account) return Float;
private
   type Account is record
      Balance : Float := 0.0;
      Number  : Integer;
   end record;
end Bank_Account;

优点

  • 隐藏实现细节,降低耦合
  • 可以更改内部结构而不影响外部代码
  • 强制通过子程序访问数据,确保一致性

3.2 受限私有类型

limited private 更进一步,禁止赋值和相等比较操作:

package File_System is
   type File_Handle is limited private;
   procedure Open (F : out File_Handle; Name : String);
   procedure Close (F : in out File_Handle);
   procedure Read (F : in File_Handle; Data : out String);
private
   type File_Handle is record
      FD : Integer;
      Path : String (1 .. 256);
   end record;
end File_System;

3.3 子类型的可见性

包体中声明的类型、常量、子程序对外部不可见,实现真正的封装。


四、包的初始化

4.1 包的执行部分

包体可以包含初始化代码,放在 beginend 之间。这些代码在程序启动时、使用该包之前自动执行。

package Database is
   procedure Connect;
   procedure Disconnect;
private
   type Connection is ...
end Database;

package body Database is
   Conn : Connection;
   
   procedure Connect is ...;
   procedure Disconnect is ...;
begin
   -- 初始化代码
   Connect;
   Ada.Text_IO.Put_Line ("Database initialized");
end Database;

4.2 多个包的初始化顺序

Ada 保证包的初始化按依赖关系有序进行(被依赖的包先初始化)。复杂情况下可使用 pragma Elaboratepragma Elaborate_All 控制。

with Logging;
package Application is
   ...
end Application;
pragma Elaborate_Body (Application);  -- 强制包体立即 elaborate

五、子程序可见性

5.1 私有子程序

在包规范中声明的子程序是公开的。如果仅在包体内声明的子程序,则只在包内部可见:

package body Math_Utils is
   -- 私有函数,仅包内可用
   function Internal_Helper (X : Float) return Float is
   begin
      return X * 2.0;
   end Internal_Helper;
   
   -- 公开函数
   function Square (X : Float) return Float is
   begin
      return Internal_Helper (X) * X;  -- 调用私有函数
   end Square;
end Math_Utils;

5.2 子程序的可见性规则

  • 包规范中声明的子程序:外部可见,需要 with
  • 包体中声明的子程序:仅包内部可见
  • 包体中的前向声明:允许子程序互相调用

六、包的重命名

6.1 renames 子句

可以为包或子程序提供本地别名,简化使用或解决命名冲突:

with Ada.Text_IO;
procedure Main is
   package IO renames Ada.Text_IO;
   procedure Put_Line (S : String) renames IO.Put_Line;
begin
   Put_Line ("Hello");   -- 调用 Ada.Text_IO.Put_Line
end Main;

6.2 重命名子程序

function Max (A, B : Integer) return Integer renames Math_Utils.Maximum;

七、子包与嵌套包

7.1 嵌套包

包可以嵌套在其他包内部,实现更细粒度的组织:

package Outer is
   procedure P;
   
   package Inner is
      procedure Q;
   end Inner;
end Outer;

package body Outer is
   procedure P is
   begin
      Inner.Q;
   end P;
   
   package body Inner is
      procedure Q is
      begin
         Ada.Text_IO.Put_Line ("Inner");
      end Q;
   end Inner;
end Outer;

7.2 子包与可见性

子包可以访问父包中声明的内容,父包不能访问子包的私有内容。


八、完整示例:几何库

-- 文件名: geometry.ads
-- 几何形状库的包规范

package Geometry is
   
   type Point is record
      X, Y : Float;
   end record;
   
   type Shape_Kind is (Circle, Rectangle);
   
   type Shape (Kind : Shape_Kind) is record
      Center : Point;
      case Kind is
         when Circle =>
            Radius : Float;
         when Rectangle =>
            Width, Height : Float;
      end case;
   end record;
   
   -- 面积计算
   function Area (S : Shape) return Float;
   
   -- 周长计算
   function Perimeter (S : Shape) return Float;
   
   -- 移动形状
   procedure Move (S : in out Shape; Delta_X, Delta_Y : Float);
   
   -- 判断点是否在形状内部
   function Contains (S : Shape; P : Point) return Boolean;
   
private
   -- 辅助常量
   Pi : constant Float := 3.141592653589793;
end Geometry;
-- 文件名: geometry.adb
-- 几何库的包体

with Ada.Numerics.Generic_Elementary_Functions;

package body Geometry is
   
   -- 私有函数:计算两点间距离
   function Distance (P1, P2 : Point) return Float is
   begin
      return (P2.X - P1.X) ** 2 + (P2.Y - P1.Y) ** 2;
   end Distance;
   
   -- 面积实现
   function Area (S : Shape) return Float is
   begin
      case S.Kind is
         when Circle =>
            return Pi * S.Radius ** 2;
         when Rectangle =>
            return S.Width * S.Height;
      end case;
   end Area;
   
   -- 周长实现
   function Perimeter (S : Shape) return Float is
   begin
      case S.Kind is
         when Circle =>
            return 2.0 * Pi * S.Radius;
         when Rectangle =>
            return 2.0 * (S.Width + S.Height);
      end case;
   end Perimeter;
   
   -- 移动
   procedure Move (S : in out Shape; Delta_X, Delta_Y : Float) is
   begin
      S.Center.X := S.Center.X + Delta_X;
      S.Center.Y := S.Center.Y + Delta_Y;
   end Move;
   
   -- 点是否在形状内部
   function Contains (S : Shape; P : Point) return Boolean is
      Dx, Dy : Float;
   begin
      case S.Kind is
         when Circle =>
            return Distance (S.Center, P) <= S.Radius ** 2;
         when Rectangle =>
            Dx := abs (P.X - S.Center.X);
            Dy := abs (P.Y - S.Center.Y);
            return Dx <= S.Width / 2.0 and Dy <= S.Height / 2.0;
      end case;
   end Contains;
   
begin
   -- 包初始化代码
   Ada.Text_IO.Put_Line ("Geometry package initialized");
end Geometry;
-- 文件名: main.adb
-- 使用几何库的主程序

with Ada.Text_IO;
with Geometry; use Geometry;

procedure Main is
   C : Shape (Kind => Circle) := (Center => (0.0, 0.0), Radius => 5.0);
   R : Shape (Kind => Rectangle) := (Center => (10.0, 10.0), Width => 4.0, Height => 3.0);
   P : Point := (3.0, 4.0);
begin
   Ada.Text_IO.Put_Line ("Circle area: " & Float'Image (Area (C)));
   Ada.Text_IO.Put_Line ("Rectangle area: " & Float'Image (Area (R)));
   
   if Contains (C, P) then
      Ada.Text_IO.Put_Line ("Point is inside circle");
   end if;
   
   Move (C, 1.0, 1.0);
   Ada.Text_IO.Put_Line ("Circle moved to: " & 
                         Float'Image (C.Center.X) & ", " & 
                         Float'Image (C.Center.Y));
end Main;

九、本课总结

  • 是 Ada 模块化的核心,分为规范(.ads)和体(.adb
  • 包规范定义公开接口,包体实现私有细节
  • 私有类型private)实现信息隐藏,limited private 禁止赋值和比较
  • 包体可以包含初始化代码(begin 部分),程序启动时自动执行
  • 子程序可见性:规范中声明为公开,体中声明为私有
  • renames 可为包或子程序创建别名
  • 包可以嵌套,子包可访问父包声明

十、课后练习

  1. 基础包:创建 String_Utils 包,提供函数 To_UpperTo_LowerReverse,并编写测试。

  2. 私有类型:实现一个 Counter 包,私有类型 Counter 存储整数,提供 IncrementDecrementValue 函数,测试其封装性。

  3. 初始化:创建一个 Config 包,在初始化时从文件读取配置参数(可先使用硬编码),提供函数获取配置值。

  4. 子包:实现一个 Math_Extended 包,包含子包 Trigonometry(提供 SinCos)和 Algebra(提供 Solve_Quadratic)。

  5. 重命名:编写程序使用 renamesAda.Text_IO.Put_Line 重命名为 Print,并测试。


十一、下节预告

第17课|异常处理

我们将:

  • 理解异常的概念与预定义异常
  • 掌握异常处理器的编写
  • 学习异常的传播与捕获
  • 应用异常编写健壮的代码

关键术语表

包规范(package specification):包的公开接口,包含类型、子程序等声明

包体(package body):包的私有实现,包含子程序定义和初始化代码

私有类型(private type):隐藏内部结构的类型,外部无法直接访问组件

受限私有类型(limited private):禁止赋值和相等比较的私有类型

renames:为包、子程序或对象创建别名的关键字

初始化部分:包体中的 begin ... end 代码块,在程序启动时执行

子包:嵌套在其他包内部的包,用于逻辑分组

信息隐藏:封装实现细节,仅暴露必要接口的设计原则