跳轉到內容

Ada 程式設計/泛型

來自華夏公益教科書,開放的書籍,開放的世界

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

引數多型(泛型單元)

[編輯 | 編輯原始碼]

程式碼重用理念源於構建大型軟體系統的必要性,這些系統將成熟的構建塊組合在一起。程式碼的可重用性提高了軟體的生產力和質量。泛型單元是 Ada 語言支援這種特性的方式之一。泛型單元是子程式或包,它們根據在使用者例項化它們之前未定義的型別和操作來定義演算法。

注意 C++ 程式設計師:泛型單元類似於 C++ 模板。

例如,要定義一個用於交換任何(非受限)型別的變數的過程

generic
  type Element_T is private;  -- Generic formal type parameter
procedure Swap (X, Y : in out Element_T);
procedure Swap (X, Y : in out Element_T) is
  Temporary : constant Element_T := X;
begin
  X := Y;
  Y := Temporary;
end Swap;

Swap 子程式被稱為泛型。子程式規範之前是泛型形式部分,由保留字generic 後面跟著可能為空的泛型形式引數列表。宣告為泛型的實體不能直接使用,必須先例項化它們。

為了能夠使用 Swap,有必要為想要的型別建立一個例項。例如

procedure Swap_Integers is new Swap (Integer);

現在,Swap_Integers 過程可以用於 Integer 型別的變數。

泛型過程可以為所有需要的型別例項化。它可以用不同的名稱例項化,或者,如果在例項化中使用相同的識別符號,則每個宣告都將過載該過程

procedure Instance_Swap is new Swap (Float);
procedure Instance_Swap is new Swap (Day_T);
procedure Instance_Swap is new Swap (Element_T => Stack_T);

類似地,泛型包可用於實現任何型別的堆疊

generic
  Max: Positive; 
  type Element_T is private;
package Generic_Stack is
  procedure Push (E: Element_T);
  function Pop return Element_T;
end Generic_Stack;
package body Generic_Stack is
  Stack: array (1 .. Max) of Element_T;
  Top  : Integer range 0 .. Max := 0;  -- initialise to empty
  -- ...
end Generic_Stack;

可以透過這種方式定義給定大小和型別的堆疊

declare
  package Float_100_Stack is new Generic_Stack (100, Float);
  use Float_100_Stack;
begin
  Push (45.8);
  -- ...
end;

泛型引數

[編輯 | 編輯原始碼]

泛型單元聲明瞭 *泛型形式引數*,它們可以是

  • 物件(模式為 *in* 或 *in out*,但絕不是 *out*)
  • 型別
  • 子程式
  • 另一個指定泛型單元的例項。

在例項化泛型時,程式設計師為每個形式引數傳遞一個 *實際引數*。形式值和子程式可以有預設值,因此傳遞實際值是可選的。

泛型形式物件

[編輯 | 編輯原始碼]

模式為 *in* 的形式引數接受指定型別的所有值、常量或變數。實際引數被複制到泛型例項中,並在泛型內部充當常量;這意味著指定型別不能受限。可以指定一個預設值,例如

generic
   Object : in Natural := 0;

對於模式為 *in out*,實際引數必須是變數。

泛型形式物件的一個限制是它們從不被視為靜態,即使實際引數恰好是靜態的。如果該物件是一個數字,則不能使用它來建立新的型別。但是,它可以用於建立新的派生型別或子型別

generic
   Size : in Natural := 0;
package P is
   type T1 is mod Size; -- illegal!
   type T2 is range 1 .. Size; -- illegal!
   type T3 is new Integer range 1 .. Size; -- OK
   subtype T4 is Integer range 1 .. Size; -- OK
end P;

形式物件是非靜態的原因是允許編譯器只為泛型生成一次目的碼,並讓所有例項共享它,並將其實際物件的地址作為引數傳遞給它。這種編譯器技術被稱為 *共享泛型*。如果形式物件是靜態的,編譯器將不得不為每個例項生成目的碼的一個副本,其中包含該物件,這可能會導致目的碼大小急劇膨脹(*程式碼膨脹*)。

(注意 C++ 程式設計師:在 C++ 中,由於形式物件可以是靜態的,因此編譯器在一般情況下不能實現共享泛型;它必須檢查泛型的整個主體,才能決定是否共享其目的碼。相反,Ada 泛型被設計成這樣,即編譯器可以在 *不檢視其主體* 的情況下例項化泛型。)

泛型形式型別

[編輯 | 編輯原始碼]

語法允許程式設計師指定哪些型別類別可以作為實際引數。根據經驗:語法表達了泛型如何看待型別,即它假設最壞的情況,而不是例項建立者如何看待型別。

這是 RM 12.5 [註釋] 的語法

 formal_type_declaration ::=
   type defining_identifier[discriminant_part] is formal_type_definition;
 
 formal_type_definition ::= formal_private_type_definition
                          | formal_derived_type_definition
                          | formal_discrete_type_definition
                          | formal_signed_integer_type_definition
                          | formal_modular_type_definition
                          | formal_floating_point_definition
                          | formal_ordinary_fixed_point_definition
                          | formal_decimal_fixed_point_definition
                          | formal_array_type_definition
                          | formal_access_type_definition
                          | formal_interface_type_definition

這很複雜,因此下面給出一些示例。使用語法 type T (<>) 宣告的型別表示具有 *未知辨別符* 的型別。這是 Ada 語言中不定型別的術語,即不能在沒有給出初始表示式的型別,例如具有沒有預設值的辨別符的型別,另一個示例是非約束陣列型別。

泛型形式型別 可接受的實際型別
type T (<>) 有限 私有; 任何型別。實際型別可以是 有限的 或不是,不定或確定,但 *泛型* 將其視為有限的和不定的,即不假定該型別可以使用賦值。
type T (<>) 私有; 任何非受限型別:泛型知道可以為這種型別的變數賦值,但不能在沒有初始值的情況下宣告這種型別的物件。
type T 私有; 任何非受限確定型別:泛型知道可以為這種型別的變數賦值,並且可以在沒有初始值的情況下宣告物件。
type T (<>) 抽象的 標記的 有限 私有; 任何 標記型別,抽象或具體,有限或不是。
type T (<>) 標記的 有限 私有; 任何具體標記型別,有限或不是。
type T (<>) 抽象的 標記的 私有; 任何非受限標記型別,抽象或具體。
type T (<>) 標記的 私有; 任何非受限、具體標記型別。
type T (<>) new Parent; Parent 派生的任何型別。泛型知道 Parent 的操作,因此可以呼叫它們。TParent 都不能是抽象的。
type T (<>) 抽象的 new Parent 私有; Parent 派生的任何型別,抽象或具體,其中 Parent 是標記型別,因此對 T 的操作的呼叫可以動態排程。
type T (<>) new Parent 私有; 從標記型別 Parent 派生的任何具體型別。
type T (<>); 任何離散型別:整數列舉
type T 範圍 <>; 任何帶符號整數型別
type T <>; 任何模型別
type T 增量 <>; 任何(非十進位制)定點型別
type T 增量 <> 位數 <>; 任何十進位制定點型別
type T 位數 <>; 任何 浮點型別
type T array (I)of E; 任何 陣列型別,其索引型別為 I,元素型別為 EIE 也可以是形式引數)
type T access O; 任何指向 O 型別物件的 訪問型別O 也可以是形式引數)

在主體中,我們只能使用為形式引數的型別類別預定義的操作。也就是說,泛型規範是泛型實現者與例項化泛型單元的客戶端之間的契約。這與其他語言(如 C++)的引數特性不同。

可以透過以下方式進一步限制可接受的實際型別集

泛型形式型別 可接受的實際型別
type T (<>)... 確定或不定型別(簡單來說:有或沒有辨別符的型別,但存在其他形式的不確定性)
type T (D : DT)... 具有 DT 型別辨別符的型別(也可以指定多個辨別符)
type T... 確定型別(簡單來說,沒有辨別符或具有具有預設值的辨別符的型別)

泛型形式子程式

[編輯 | 編輯原始碼]

可以將子程式作為引數傳遞給泛型。泛型指定一個泛型形式子程式,包含引數列表和返回型別(如果子程式是函式)。實際引數必須與該引數配置檔案匹配,但引數的名稱不必匹配。

這是一個將另一個子程式作為引數的泛型子程式的規範

generic
  type Element_T is private;
  with function "*" (X, Y: Element_T) return Element_T;
function Square (X : Element_T) return Element_T;

這是泛型子程式的主體;它像呼叫任何其他子程式一樣呼叫引數。

function Square (X: Element_T) return Element_T is
begin
  return X * X;   -- The formal operator "*".
end Square;

例如,這個泛型函式可以與矩陣一起使用,假設已經定義了矩陣乘積。

with Square;
with Matrices;
procedure Matrix_Example is
  function Square_Matrix is new Square
    (Element_T => Matrices.Matrix_T, "*" => Matrices.Product);
  A : Matrices.Matrix_T := Matrices.Identity;
begin
  A := Square_Matrix (A);
end Matrix_Example;

可以使用“方框”(is <>)來指定預設值,例如

generic
  type Element_T is private;
  with function "*" (X, Y: Element_T) return Element_T is <>;

這意味著,如果在例項化時,實際型別存在一個函式“*”,並且該函式是直接可見的,那麼它將被預設用作實際子程式。

主要用途之一是傳遞需要的運算子。以下示例顯示了這一點(請點選下載連結檢視完整示例)

檔案:演算法/二分查詢.adb (檢視, 純文字, 下載頁面, 瀏覽所有)
  generic
     type Element_Type is private;
     ...
     with function "<"
       (Left  : in Element_Type;
        Right : in Element_Type)
        return  Boolean
     is <>;
  procedure Search
    (Elements : in Array_Type;
     Search   : in Element_Type;
     Found    : out Boolean;
     Index    : out Index_Type'Base)
     ...

其他泛型包的泛型例項

[edit | edit source]

泛型形式可以是包;它必須是泛型包的例項,以便泛型知道包匯出的介面

generic
   with package P is new Q (<>);

這意味著實際引數必須是泛型包 Q 的例項。Q 後的方框表示我們不關心用於建立 P 的實際引數。可以指定確切的引數,也可以指定必須使用預設值,如下所示

generic
   -- P1 must be an instance of Q with the specified actual parameters:
   with package P1 is new Q (Param1 => X, Param2 => Y);

   -- P2 must be an instance of Q where the actuals are the defaults:
   with package P2 is new Q;

可以指定一個預設引數、無引數或只指定一些引數。預設值用方框 (“ => <> ”) 表示,可以使用 “ others => <> ”) 表示 “ 對所有未提及的引數使用預設值”。當然,實際包必須與這些約束匹配。

泛型可以看到實際包的公共部分和泛型引數(以上示例中的 Param1 和 Param2)。

此功能允許程式設計師將任意複雜的型別作為引數傳遞給泛型單元,同時保持完整的型別安全性和封裝。(需要示例)

包不能將自身列為泛型形式,因此無法進行泛型遞迴。以下操作是非法的

with A;
generic
   with package P is new A (<>);
package A; -- illegal: A references itself

事實上,這只是

with A; -- illegal: A does not exist yet at this point!
package A;

的一個特例,這也是非法的,儘管 A 已經不再是泛型。

例項化泛型

[edit | edit source]

要例項化泛型單元,請使用關鍵字 new

function Square_Matrix is new Square
   (Element_T => Matrices.Matrix_T, "*" => Matrices.Product);

對 C++ 程式設計師特別感興趣的注意事項

  • 泛型形式型別完全定義了哪些型別可以作為實際引數;因此,編譯器可以在不檢視泛型主體的情況下例項化泛型。
  • 每個例項都有一個名稱,與所有其他例項不同。特別是,如果一個泛型包聲明瞭一個型別,並且建立了該包的兩個例項,那麼即使實際引數相同,也將獲得兩個不同的、不相容的型別。
  • Ada 要求所有例項化必須是顯式的。
  • 無法建立泛型的特殊情況例項(在 C++ 中稱為“模板特化”。

作為上述內容的結果,Ada 不允許模板超程式設計。但是,這種設計具有顯著的優勢

  • 除非程式設計師要求內聯子程式,否則所有泛型例項都可以共享目的碼;不存在程式碼膨脹的風險。
  • 在閱讀其他人編寫的程式時,沒有隱藏的例項化,也沒有特殊情況需要擔心。Ada 遵循最小驚奇原則。

高階泛型

[edit | edit source]

泛型和巢狀

[edit | edit source]

泛型單元可以巢狀在另一個單元中,該單元本身可能是泛型的。即使沒有特殊的規則適用(只有關於泛型的正常規則和關於巢狀單元的規則),新手也可能感到困惑。理解泛型單元和泛型單元的例項之間的區別很重要。

示例 1. 巢狀在非泛型包中的泛型子程式。

package Bag_Of_Strings is
   type Bag is private;
   generic
      with procedure Operator (S : in out String);
   procedure Apply_To_All (B : in out Bag);
private
   -- omitted
end Bag_Of_Strings;

要使用 Apply_To_All,首先定義要應用於 Bag 中每個 String 的過程。然後,例項化 Apply_To_All,最後呼叫例項。

with Bag_Of_Strings;
procedure Example_1 is
   procedure Capitalize (S : in out String) is separate; -- omitted
   procedure Capitalize_All is
      new Bag_Of_Strings.Apply_To_All (Operator => Capitalize);
   B : Bag_Of_Strings.Bag;
begin
   Capitalize_All (B);
end Example_1;

示例 2. 巢狀在泛型包中的泛型子程式

這與上面的示例相同,只是現在 Bag 本身是泛型的

generic
   type Element_Type (<>) is private;
package Generic_Bag is
   type Bag is private;
   generic
      with procedure Operator (S : in out Element_Type);
   procedure Apply_To_All (B : in out Bag);
private
   -- omitted
end Generic_Bag;

如您所見,泛型形式子程式 Operator 接受泛型形式型別 Element_Type 的引數。這是可以的:巢狀泛型可以看到其封閉單元中的所有內容。

您不能直接例項化 Generic_Bag.Apply_To_All,因此必須首先建立 Generic_Bag 的例項,例如 Bag_Of_Strings,然後例項化 Bag_Of_Strings.Apply_To_All

with Generic_Bag;
procedure Example_2 is
   procedure Capitalize (S : in out String) is separate; -- omitted
   package Bag_Of_Strings is
      new Generic_Bag (Element_Type => String);
   procedure Capitalize_All is
      new Bag_Of_Strings.Apply_To_All (Operator => Capitalize);
   B : Bag_Of_Strings.Bag;
begin
   Capitalize_All (B);
end Example_2;

泛型和子單元

[edit | edit source]

示例 3. 作為非泛型單元的子單元的泛型單元。

泛型子單元的每個例項都是父單元的子單元,因此它可以看到父單元的公共部分和私有部分。

package Bag_Of_Strings is
   type Bag is private;
private
   -- omitted
end Bag_Of_Strings; 

generic
   with procedure Operator (S : in out String);
procedure Bag_Of_Strings.Apply_To_All (B : in out Bag);

與示例 1 的區別在於

  • Bag_Of_Strings.Apply_To_All 是單獨編譯的。特別是,Bag_Of_Strings.Apply_To_All 可能由沒有訪問 Bag_Of_Strings 源文字的人員編寫。
  • 在使用 Bag_Of_Strings.Apply_To_All 之前,必須顯式地 with 它;with 父單元 Bag_Of_Strings 不足以。
  • 如果不使用 Bag_Of_Strings.Apply_To_All,程式中將不包含其目的碼。
  • 由於 Bag_Of_Strings.Apply_To_All 位於庫級別,因此它可以宣告受控型別;巢狀包在 Ada 95 中無法做到這一點。在 Ada 2005 中,可以在任何級別宣告受控型別。
with Bag_Of_Strings.Apply_To_All; -- implicitly withs Bag_Of_Strings, too
procedure Example_3 is
   procedure Capitalize (S : in out String) is separate; -- omitted
   procedure Capitalize_All is
      new Bag_Of_Strings.Apply_To_All (Operator => Capitalize);
   B : Bag_Of_Strings.Bag;
begin
   Capitalize_All (B);
end Example_3;

示例 4. 作為泛型單元的子單元的泛型單元

這與示例 3 相同,只是現在 Bag 也是泛型的。

generic
   type Element_Type (<>) is private;
package Generic_Bag is
   type Bag is private;
private
   -- omitted
end Generic_Bag;

generic
   with procedure Operator (S : in out Element_Type);
procedure Generic_Bag.Apply_To_All (B : in out Bag);

with Generic_Bag.Apply_To_All;
procedure Example_4 is
   procedure Capitalize (S : in out String) is separate; -- omitted
   package Bag_Of_Strings is
      new Generic_Bag (Element_Type => String);
   procedure Capitalize_All is
      new Bag_Of_Strings.Apply_To_All (Operator => Capitalize);
   B : Bag_Of_Strings.Bag;
begin
   Capitalize_All (B);
end Example_4;

示例 5. 無引數泛型子單元

泛型單元的子單元 必須 是泛型的,無論如何。仔細想想,這是非常合乎邏輯的:子單元可以看到其父單元的公共部分和私有部分,包括在父單元中宣告的變數。如果父單元是泛型的,那麼子單元應該看到哪個例項?答案是,子單元必須是父單元的單個例項的子單元,因此子單元也必須是泛型的。

generic
   type Element_Type (<>) is private;
   type Hash_Type is (<>);
   with function Hash_Function (E : Element_Type) return Hash_Type;
package Generic_Hash_Map is
   type Map is private;
private
   -- omitted
end Generic_Hash_Map;

假設我們想要 Generic_Hash_Map 的一個子單元,它可以將對映序列化到磁碟;為此,它需要按雜湊值對對映進行排序。這很容易做到,因為我們知道 Hash_Type 是離散型別,因此它具有小於運算子。執行序列化的子單元不需要任何額外的泛型引數,但它必須是泛型的,因此它可以看到其父單元的泛型引數、公共部分和私有部分。

generic
package Generic_Hash_Map.Serializer is
    procedure Dump (Item : in Map; To_File : in String);
    procedure Restore (Item : out Map; From_File : in String);
end Generic_Hash_Map.Serializer;

要將對映讀寫到磁碟,首先要建立 Generic_Hash_Map 的例項,例如 Map_Of_Unbounded_Strings,然後建立 Map_Of_Unbounded_Strings.Serializer 的例項

with Ada.Strings.Unbounded;
with Generic_Hash_Map.Serializer;
procedure Example_5 is
   use Ada.Strings.Unbounded;
   function Hash (S : in Unbounded_String) return Integer is separate; -- omitted
   package Map_Of_Unbounded_Strings is
      new Generic_Hash_Map (Element_Type => Unbounded_String,
                            Hash_Type => Integer,
                            Hash_Function => Hash);
   package Serializer is
      new Map_Of_Unbounded_Strings.Serializer;
   M : Map_Of_Unbounded_Strings.Map;
begin
   Serializer.Restore (Item => M, From_File => "map.dat");
end Example_5;

另請參閱

[edit | edit source]

華夏公益教科書

[edit | edit source]

維基百科

[edit | edit source]

Ada 參考手冊

[edit | edit source]
華夏公益教科書