跳转到内容

Ada 编程/包

来自维基教科书,开放世界中的开放书籍

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

Ada 鼓励将代码划分为称为 *包* 的独立模块。每个包可以包含任何组合的项目。

使用包的一些好处是

  • 包内容放在一个独立的命名空间中,避免了命名冲突,
  • 可以对程序员隐藏包的实现细节(信息隐藏),
  • 面向对象需要在包中定义一个类型及其基本子程序,以及
  • 作为库单元的包可以独立编译。

一些更常见的包用法是

  • 一组相关的子程序以及它们的共享数据,这些数据在包外部不可见,
  • 一个或多个数据类型以及用于操作这些数据类型的子程序,以及
  • 可以在不同条件下实例化的泛型包。

以下是来自当前 Ada 参考手册的引用 第 7 章:包。RM 7(1) [注释]

包是程序单元,允许指定逻辑相关的实体组。通常,一个包包含一个类型的声明(通常是一个私有类型或私有扩展),以及该类型的基本子程序的声明,这些子程序可以从包外部调用,而它们的内部工作机制对外部用户隐藏。

独立编译

[编辑 | 编辑源代码]

注意:以下章节讨论 *库级* 的包。这是最常见的用法,因为包是 Ada 中基本的代码结构化方式。然而,Ada 作为一种面向块的语言,可以在任何声明区域的任何级别声明包。在这种情况下,正常的可见性规则也适用于包体。

库级的包规范和包体是编译单元,因此可以独立编译。Ada 没有规定编译单元如何以及在何处存储。这是实现相关的。(大多数实现确实将编译单元存储在它们自己的文件中;名称后缀会有所不同,GNAT 使用 .ads.adbAPEX .1.ada.2.ada。)包体本身可以被分成多个部分,通过指定子程序实现或嵌套包的体是 **独立的**。然后它们是进一步的编译单元。

Ada 相对于大多数其他编程语言的最大优势之一是其定义明确的模块化和独立编译系统。即使 Ada 允许独立编译,它通过强制编译顺序和兼容性检查来维护各种编译之间强类型检查。Ada 编译器确定编译顺序;不需要 make 文件。Ada 使用独立编译(如 Modula-2JavaC#),而不是独立编译(如 C/C++ 所做的那样),在独立编译中,各个部分在编译时不知道将要与之组合的其他编译单元。

给 C/C++ 用户的说明:是的,你可以使用预处理器来模拟独立编译 - 但这仅仅是一种模拟,最小的错误也会导致难以发现的错误。值得注意的是,包括 D 在内的所有 C/C++ 后继语言都放弃了独立编译和使用预处理器。

因此,了解 Ada 从 Ada-83 开始就拥有独立编译,并且它可能是周围最复杂的实现,这是一件好事。

包的组成部分

[编辑 | 编辑源代码]

一个包通常由两个部分组成,规范和包体。包规范可以进一步划分为两个逻辑部分,可见部分和私有部分。包规范中只有可见部分是必须的。包规范的私有部分是可选的,一个包规范可能没有包体 - 包体只存在于完成规范中任何 *不完整* 的项目。子程序声明是最常见的 *不完整* 项目。如果没有不完整的声明,则不能有包体,如果规范中存在一些不完整的声明,则必须有包体。

要理解三方分割的价值,请考虑一个已经发布并正在使用的包的情况。对规范的可见部分的更改将要求所有使用软件的程序员验证更改是否不会影响使用代码。对声明的私有部分的更改将要求所有使用代码重新编译,但通常不需要审查。私有部分的一些更改可能会改变客户端代码的含义。例如,将私有记录类型更改为私有访问类型。这个改变可以通过对私有部分的改变来完成,但是改变了客户端代码中赋值的语义含义。对包体的更改只需要重新编译包含包体的文件,因为 *包体外部* 的任何内容都无法访问包体内部的任何内容(超出规范部分中的声明)。

三部分的一个常见用法是在可见部分声明类型的存在和一些操作该类型的子程序,在私有部分定义类型的实际结构(例如作为记录),并在包体中提供实现子程序的代码。

包规范 - 可见部分

[编辑 | 编辑源代码]

包规范的可见部分描述了所有对任何希望使用该包的人可见的子程序规范、变量、类型、常量等。

package Public_Only_Package is

  type Range_10 is range 1 .. 10;

end Public_Only_Package;

由于 Range_10 是一个整型,因此在此包中隐式声明了许多操作。

私有部分

[编辑 | 编辑源代码]

包的私有部分有两个目的

  • 完成私有类型和常量的延迟定义。
  • 导出仅对包子级可见的实体。
package Package_With_Private is
     
   type Private_Type is private;

private

   type Private_Type is array (1 .. 10) of Integer;

end Package_With_Private;

由于该类型是私有的,因此只要在可见部分没有定义任何操作,客户端就不能使用它。

包体定义了包的实现。规范中定义的所有子程序都必须在包体中实现。可以在包体中定义对包用户不可见的新子程序、类型和对象。

package Package_With_Body is

   type Basic_Record is private;

   procedure Set_A (This : in out Basic_Record;
                    An_A : in     Integer);

   function Get_A (This : Basic_Record) return Integer;

private

   type Basic_Record is 
      record 
         A : Integer;
      end record ;

   pragma Pure_Function  (Get_A);  -- not a standard Ada pragma
   pragma Inline (Get_A);
   pragma Inline (Set_A);

end Package_With_Body;
package body Package_With_Body is

   procedure Set_A (This : in out Basic_Record;
                    An_A : in     Integer) is
   begin
      This.A := An_A;
   end Set_A;

   function Get_A (This : Basic_Record) return Integer is
   begin
      return This.A;
   end Get_A;

end Package_With_Body;
pragma Pure_Function
仅在使用 GNAT 时可用。

两种包

[编辑 | 编辑源代码]

以上每个包都定义了一种类型及其操作。当类型的组成被放置在包的私有部分时,该包就会导出一个称为抽象数据类型或简称为 ADT 的东西。然后通过调用与相应类型关联的子程序之一来构造该类型的对象。

另一种类型的包是抽象状态机或 ASM。一个包将对问题域中的单个项目进行建模,例如汽车的发动机。如果一个程序控制一辆汽车,通常只有一个发动机,或者说是唯一的发动机。包规范的公共部分只声明模块(例如发动机的模块)的操作,但没有类型。所有模块数据都隐藏在包体中,在那里它们充当状态变量,供包的子程序查询或操作。初始化部分将状态变量设置为其初始值。

package Package_With_Body is

   procedure Set_A (An_A : in Integer);

   function Get_A return Integer;

private

   pragma Pure_Function (Get_A);—not a standard Ada pragma

end Package_With_Body;
package body Package_With_Body is

   The_A: Integer;

   procedure Set_A (An_A : in Integer) is
   begin
      The_A := An_A;
   end Set_A;

   function Get_A return Integer is
   begin
      return The_A;
   end Get_A;


begin

   The_A := 0;

end Package_With_Body;

(关于构造的说明:包初始化部分位于begin 之后,对应于 ADT 包的构造子程序。但是,由于状态机本身就是一个“对象”,因此“构造”发生在包初始化期间。(这里它设置状态变量The_A到其初始值。)ASM 包可以看作是一个单例。)

使用包

[edit | edit source]

要使用一个包,需要在with子句中命名它,而要直接访问该包,需要在use子句中命名它。

对于 C++ 程序员来说,Ada 的with子句类似于 C++ 预处理器的#include,而 Ada 的use类似于 C++ 中的using namespace语句。特别是,use会导致与using namespace相同的命名空间污染问题,因此应该谨慎使用。重命名可以将长复合名称缩短到易于管理的长度,而use type子句使类型的运算符可见。这些功能减少了对普通use的需求。

标准 with

[edit | edit source]

标准 with 子句为以下定义的单元提供对单元公共部分的可见性。导入的包可以在定义单元的任何部分使用,包括当子句用于规范时,在主体中使用。

私有 with

[edit | edit source]

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

private with Ada.Strings.Unbounded; 

package Private_With is

   -- The package Ada.String.Unbounded is not visible at this point

   type Basic_Record is private;

   procedure Set_A (This : in out Basic_Record;
                    An_A : in     String);

   function Get_A (This : Basic_Record) return String;

private
   -- The visibility of package Ada.String.Unbounded starts here

   package Unbounded renames Ada.Strings.Unbounded;

   type Basic_Record is 
      record 
         A : Unbounded.Unbounded_String;
      end record;

   pragma Pure_Function  (Get_A);
   pragma Inline (Get_A);
   pragma Inline (Set_A);

end Private_With;
package body Private_With is

   -- The private withed package is visible in the body too

   procedure Set_A (This : in out Basic_Record;
                    An_A : in     String)
   is
   begin
      This.A := Unbounded.To_Unbounded_String (An_A);
   end Set_A;

   function Get_A (This : Basic_Record) return String is
   begin
      return Unbounded.To_String (This.A);
   end Get_A;

end Private_With;

受限 with

[edit | edit source]

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

受限 with 可用于表示位于不同包中的两个(或更多)相互依赖的类型。

limited with Departments;

package Employees is

   type Employee is tagged private;

   procedure Assign_Employee
     (E : in out Employee;
      D : access Departments.Department'Class);

   type Dept_Ptr is access all Departments.Department'Class;

   function Current_Department(E : in Employee) return Dept_Ptr;
   ...
end Employees;
limited with Employees;

package Departments is

   type Department is tagged private;

   procedure Choose_Manager
     (Dept    : in out Department;
      Manager : access Employees.Employee'Class);
   ...
end Departments;

使运算符可见

[edit | edit source]

假设您有一个定义了某种数值类型 T 的 Universe 包。

with Universe;
procedure P is
  V: Universe.T := 10.0;
begin
  V := V * 42.0;  --  illegal
end P;

此程序片段是非法的,因为 Universe 中隐式定义的运算符不可直接访问。

您有四种选择使程序合法。

使用 use_package_clause。这会使 Universe 中所有声明直接可见(前提是它们没有因其他同名词而隐藏)。

with Universe;
use  Universe;
procedure P is
  V: Universe.T := 10.0;
begin
  V := V * 42.0;
end P;

使用重命名。这样做容易出错,因为如果您重命名了许多运算符,则可能出现剪切粘贴错误。

with Universe;
procedure P is
  function "*" (Left, Right: Universe.T) return Universe.T renames Universe."*";
  function "/" (Left, Right: Universe.T) return Universe.T renames Universe."*";  --  oops
  V: Universe.T := 10.0;
begin
  V := V * 42.0;
end P;

使用限定符。这样做非常丑陋,难以阅读。

with Universe;
procedure P is
  V: Universe.T := 10.0;
begin
  V := Universe."*" (V, 42.0);
end P;

使用 use_type_clause。这将只使 Universe 中的运算符直接可见。

with Universe;
procedure P is
  V: Universe.T := 10.0;
  use type Universe.T;
begin
  V := V * 42.0;
end P;

use_type_clause 有一个特殊的美感。假设您有一组如下所示的包

with Universe;
package Pack is
  subtype T is Universe.T;
end Pack;
with Pack;
procedure P is
  V: Pack.T := 10.0;
begin
  V := V * 42.0;  --  illegal
end P;

现在您遇到了麻烦。由于 Universe 没有被显示,您不能使用 use_package_clause 访问 Universe 来使运算符直接可见,也不能使用限定符来访问,原因相同。此外,Pack 的 use_package_clause 也没有帮助,因为运算符没有定义在 Pack 中。上述结构的效果是运算符不可命名,即它不能在重命名语句中被重命名。

当然,您可以将 Universe 添加到上下文子句中,但这可能由于其他原因(例如编码标准)而不可能;此外,将运算符添加到 Pack 中也可能是禁止的或不可行的。那么该怎么办呢?

解决方案很简单。使用 Pack.T 的 use_type_clause,一切就都好了!

with Pack;
procedure P is
  V: Pack.T := 10.0;
  use type Pack.T;
begin
  V := V * 42.0;
end P;

包组织

[edit | edit source]

嵌套包

[edit | edit source]

嵌套包是在包内部声明的包。与普通包一样,它也有公共部分和私有部分。从外部来看,在嵌套包 N 中声明的项目将像往常一样具有可见性;程序员可以使用完整的点分隔名称(例如 P.N.X)来引用这些项目。(但不能使用 P.M.Y。)

package P is
   D: Integer;

   --  a nested package:
   package N is
      X: Integer;
   private
      Foo: Integer;
   end N;

   E: Integer;
private
   --  another nested package:
   package M is
      Y: Integer;
   private
      Bar: Integer;
   end M;

end P;

在包内部,声明在它们被引入时按文本顺序变得可见。也就是说,在其他声明 D 之后声明的嵌套包 N 可以引用该声明 D。在 N 之后声明的声明 E 可以引用 N 的项目。[1] 但两者都不能“提前”引用任何在它们之后的声明。例如,上面的规范 N 不能以任何方式引用 M

在以下示例中,在两个嵌套包中都派生了类型DisksBooks。请注意,父类型的完整声明Item出现在两个嵌套包之前。

with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;

package Shelf is

   pragma Elaborate_Body;

   --  things to put on the shelf

   type ID is range 1_000 .. 9_999;
   type Item (Identifier : ID) is abstract tagged limited null record;
   type Item_Ref is access constant Item'class;

   function Next_ID return ID;
   --  a fresh ID for an Item to Put on the shelf


   package Disks is

      type Music is (
         Jazz,
         Rock,
         Raga,
         Classic,
         Pop,
         Soul);

      type Disk (Style : Music; Identifier : ID) is new Item (Identifier)
         with record
            Artist : Unbounded_String;
            Title  : Unbounded_String;
         end record;

   end Disks;


   package Books is

      type Literature is (
         Play,
         Novel,
         Poem,
         Story,
         Text,
         Art);

      type Book (Kind : Literature; Identifier : ID) is new Item (Identifier)
         with record
            Authors : Unbounded_String;
            Title   : Unbounded_String;
            Year    : Integer;
         end record;

   end Books;

   --  shelf manipulation

   procedure Put (it: Item_Ref);
   function Get (identifier : ID) return Item_Ref;
   function Search (title : String) return ID;

private

   --  keeping private things private

   package Boxes is
      type Treasure(Identifier: ID) is limited private;
   private
      type Treasure(Identifier: ID) is new Item(Identifier) with null record;
   end Boxes;

end Shelf;

包也可以嵌套在子程序中。事实上,包可以在任何声明部分声明,包括块的声明部分。

子包

[edit | edit source]

Ada 允许使用所谓的子包(子包)来扩展单元(包)的功能。除了一些例外,所有父包的功能都可用于子包。这意味着父包的所有公共和私有声明都对所有子包可见。

上面的示例,重新设计为包层次结构,如下所示。请注意,包 Ada.Strings.Unbounded 在顶层包Shelf中不需要,因此它的 with 子句没有出现在这里。(我们添加了一个匹配函数用于搜索货架,虽然)


package Shelf is

   pragma Elaborate_Body;

   type ID is range 1_000 .. 9_999;
   type Item (Identifier : ID) is abstract tagged limited null record;
   type Item_Ref is access constant Item'Class;

   function Next_ID return ID;
   --  a fresh ID for an Item to Put on the shelf

   function match (it : Item; Text : String) return Boolean is abstract;
   --  see whether It has bibliographic information matching Text


   --   shelf manipulation

   procedure Put (it: Item_Ref);
   function Get (identifier : ID) return Item_Ref;
   function Search (title : String) return ID;

end Shelf;

子包的名称由父单元的名称后跟子包的标识符组成,两者之间用句点(点)`.` 分隔。

with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;

package Shelf.Books is

   type Literature is (
      Play,
      Novel,
      Poem,
      Story,
      Text,
      Art);

   type Book (Kind : Literature; Identifier : ID) is new Item (Identifier)
      with record
         Authors : Unbounded_String;
         Title   : Unbounded_String;
         Year    : Integer;
      end record;

   function match(it: Book; text: String) return Boolean;

end Shelf.Books;

Book有两个类型为Unbounded_String的组件,因此 Ada.Strings.Unbounded 出现在子包的 with 子句中。这与嵌套包的情况不同,嵌套包要求所有由任何一个嵌套包需要的单元都列在封闭包的上下文子句中(参见 10.1.2 上下文子句 - With 子句 (Annotated))。因此,子包可以更好地控制包依赖关系。With 子句更局限。

新的子包Shelf.Disks看起来很像。该Boxes包在原始Shelf包的私有部分中是嵌套包,现在移到了私有子包

private package Shelf.Boxes is
    type Treasure(Identifier: ID) is limited private;
private
    type Treasure(Identifier: ID) is new Item(Identifier) with null record;
    function match(it: Treasure; text: String) return Boolean;
end Shelf.Boxes;

中。包的私有性意味着它只能由同样私有的客户端单元使用。这些客户端包括私有兄弟姐妹以及兄弟姐妹的主体(因为主体永远不是公共的)。

子包可以像普通包一样列在上下文子句中。子包的with 也'with'了父包。

子单元

[edit | edit source]

子单元只是将一个主体移到它自己的位置的功能,否则包含它的主体将变得太大。它还可以用于限制上下文子句的范围。

子单元允许将包在物理上划分为不同的编译单元,而不会破坏包的逻辑完整性。通常,每个分离的子单元都对应一个单独的文件,允许对每个子单元进行单独的编译,并且每个子单元都有独立的版本控制历史。

 package body Pack is
   procedure Proc is separate;
 end Pack;

 with Some_Unit;
 separate (Pack)
 procedure Proc is
 begin
   ...
 end Proc;
  1. 例如,E: Integer := D + N.X;

另请参阅

[编辑 | 编辑源代码]

维基教科书

[编辑 | 编辑源代码]

维基百科

[编辑 | 编辑源代码]

Ada 95 参考手册

[编辑 | 编辑源代码]

Ada 2005 参考手册

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