跳轉到內容

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 參考手冊

[編輯 | 編輯原始碼]
華夏公益教科書