跳轉到內容

Ada 程式設計/型別系統

來自 Wikibooks,開放世界中的開放書籍

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

Ada 的型別系統允許程式設計師構建強大的抽象,這些抽象代表現實世界,併為編譯器提供有價值的資訊,以便編譯器能夠在邏輯或設計錯誤成為 bug 之前找到它們。它是語言的核心,優秀的 Ada 程式設計師學會了利用它來獲得巨大的優勢。四個原則支配著型別系統

  • 型別:對資料進行分類的方法。字元是 'a' 到 'z' 之間的型別。整數是包括 0、1、2... 的型別。
  • 強型別:型別彼此不相容,因此不能混合蘋果和橘子。編譯器不會猜測你的蘋果是橘子。你必須明確地說 my_fruit = fruit(my_apple)。強型別減少了錯誤數量。這是因為開發人員可以非常輕鬆地將浮點數寫入整數變數而不自知。現在你程式成功執行所需的資料在編譯器切換型別時在轉換過程中丟失了。Ada 會生氣並拒絕開發人員的愚蠢錯誤,拒絕執行轉換,除非明確告知。
  • 靜態型別:在編譯時進行型別檢查,這使得能夠更早地發現型別錯誤。
  • 抽象:型別表示現實世界或所面臨的問題;而不是計算機如何以內部方式表示資料。有一些方法可以指定型別必須如何在位級別表示,但我們將把討論推遲到另一章。抽象的一個例子是你的汽車。你實際上不知道它是如何工作的,你只知道這塊笨重的金屬塊在移動。你使用的幾乎所有技術都是為了簡化構成它的複雜電路而抽象化的層 - 軟體也是如此。你想要抽象,因為類中的程式碼比除錯時沒有解釋的一百個 if 語句更有意義
  • 名稱等價:與大多數其他語言中使用的結構等價相反。如果且僅當兩個型別具有相同的名稱時,它們才相容;不是因為它們碰巧具有相同的大小或位表示。因此,你可以宣告兩個具有相同範圍但完全不相容的整數型別,或者宣告兩個具有完全相同元件但彼此不相容的記錄型別。

型別彼此不相容。但是,每個型別可以有任意數量的子型別,這些子型別與其基本型別相容,並且可能彼此相容。有關子型別之間不相容的示例,請參見下文。

預定義型別

[編輯 | 編輯原始碼]

有幾個預定義型別,但大多數程式設計師更喜歡定義自己的特定於應用程式的型別。但是,這些預定義型別作為獨立開發的庫之間的介面非常有用。顯然,預定義庫也使用這些型別。

這些型別在 標準 包中預定義

整數
此型別至少涵蓋範圍 .. (RM 3.5.4: (21) [註釋])。標準還定義了此型別的 NaturalPositive 子型別。
浮點數
此型別只有一個非常弱的實現要求 (RM 3.5.7: (14) [註釋]);大多數時候,你會定義自己的浮點型別,並指定你的精度和範圍要求。
持續時間
用於計時的一種定點型別。它以秒為單位表示一段時間 (RM A.1: (43) [註釋])。
字元
一種特殊的列舉形式。有三種預定義的字元型別:8 位字元(稱為 Character)、16 位字元(稱為 Wide_Character)和 32 位字元 (Wide_Wide_Character)。Character 從語言的第一個版本 (Ada 83) 開始就存在,Wide_CharacterAda 95 中新增,而型別 Wide_Wide_Character 可用於 Ada 2005
字串
三種不確定的 陣列型別,分別為 CharacterWide_CharacterWide_Wide_Character。標準庫包含用於處理三種變體字串的包:固定長度 (Ada.Strings.Fixed)、長度在一定上限以下變化 (Ada.Strings.Bounded) 和無界長度 (Ada.Strings.Unbounded)。每個包都有 Wide_Wide_Wide_ 變體。
布林值
Ada 中的 Boolean列舉,包含 FalseTrue,具有特殊語義。

系統系統.儲存元素 預定義了一些主要用於低階程式設計和與硬體介面的型別。

System.Address
記憶體中的地址。
System.Storage_Elements.Storage_Offset
偏移量,可以將其新增到地址以獲取新地址。 您也可以從另一個地址中減去一個地址以獲取它們之間的偏移量。 AddressStorage_Offset 及其相關的子程式共同提供了地址算術。
System.Storage_Elements.Storage_Count
Storage_Offset 的一個子型別,它不能為負數,並且表示資料結構的記憶體大小(類似於 C 的 size_t)。
System.Storage_Elements.Storage_Element
在大多數計算機中,這是一個位元組。 正式地,它是具有地址的最小記憶體單元。
System.Storage_Elements.Storage_Array
一個 Storage_Element 的陣列,沒有任何意義,在進行原始記憶體訪問時很有用。

型別層次結構

[edit | edit source]

型別按層次結構組織。 型別從層次結構中位於其上方的型別繼承屬性。 例如,所有標量型別(整數、列舉、模、定點和浮點型別)都具有 運算子 "<"、">" 以及為它們定義的算術運算子,所有離散型別都可以用作陣列索引。

Ada 型別層次結構

以下是每種型別類別的大致概述; 請按照連結獲取詳細說明。 括號內是熟悉這些語言的讀者在 C 和 Pascal 中的等價物。

有符號整數 (int, INTEGER)
有符號整數是透過 範圍 定義的。
無符號整數 (unsigned, CARDINAL)
無符號整數稱為 模型別。 除了無符號之外,它們還具有環繞功能。
列舉 (enum, char, bool, BOOLEAN)
Ada 列舉 型別是一個獨立的型別族。
浮點數 (float, double, REAL)
浮點型別由所需的 位數、相對誤差界限定義。
普通和十進位制定點 (DECIMAL)
定點型別由它們的 增量、絕對誤差界限定義。
陣列 ( [ ], ARRAY [ ] OF, STRING )
支援編譯時和執行時確定的大小的陣列。
記錄 (struct, class, RECORD OF)
記錄是一種 複合型別,它將一個或多個欄位分組。
訪問 (*, ^, POINTER TO)
Ada 的 訪問 型別可能不僅僅是一個簡單的記憶體地址。
任務和保護 (類似於 C++ 中的多執行緒)
任務和保護型別允許控制併發。
介面 (類似於 C++ 中的虛方法)
Ada 2005 中的新增功能,這些型別類似於 Java 介面。

型別分類

[edit | edit source]

Ada 的型別可以按如下方式分類。

特定型別與類範圍型別

type T is ...  --  a specific type
  T'Class      --  the corresponding class-wide type (exists only for tagged types)

T'ClassT'Class'Class 相同。

具有特定型別引數的原始操作是非分派的,而具有類範圍型別引數的原始操作是分派的。

可以透過派生特定型別來宣告新型別; 原始操作透過派生繼承。 您不能從類範圍型別派生。

約束型別與非約束型別

type I is range 1 .. 10;           --  constrained
type AC is array (1 .. 10) of ...  --  constrained
type AU is array (I range <>) of ...          --  unconstrained
type R (X: Discriminant [:= Default]) is ...  --  unconstrained

透過為非約束子型別提供約束,子型別或物件將變為約束型別。

subtype RC is R (Value);  --  constrained subtype of R
OC: R (Value);            --  constrained object of anonymous constrained subtype of R
OU: R;                    --  unconstrained object

只有在型別宣告中給出預設值時,才能宣告非約束物件。 該語言未指定如何分配此類物件。 GNAT 分配最大大小,因此大小變化(可能會隨著區分符變化而出現)不會出現問題。 另一種可能性是在堆上進行隱式動態分配,並在大小變化時重新分配,然後進行釋放。

確定型別與不確定型別

type I is range 1 .. 10;                     --  definite
type RD (X: Discriminant := Default) is ...  --  definite
type T (<>) is ...                    --  indefinite
type AU is array (I range <>) of ...  --  indefinite
type RI (X: Discriminant) is ...      --  indefinite

確定子型別允許在沒有初始值的情況下宣告物件,因為確定子型別的物件具有在建立時已知的約束。 不確定子型別的物件宣告需要初始值來提供約束; 然後,它們將透過初始值提供的約束進行約束。

OT: T  := Expr;                       --  some initial expression (object, function call, etc.)
OA: AU := (3 => 10, 5 => 2, 4 => 4);  --  index range is now 3 .. 5
OR: RI := Expr;                       --  again some initial expression as above

非約束型別與不確定型別

請注意,非約束子型別不一定是不確定的,如上面 RD 所示:它是一個確定的非約束子型別。

併發型別

[edit | edit source]

除了對資料+操作進行分類之外,Ada 語言還使用型別來實現另一個目的。 型別系統集成了併發(執行緒、並行)。 程式設計師將使用型別來表達程式的併發控制執行緒。

型別系統這部分的核心部分,任務型別和保護型別將在有關任務的部分中更深入地解釋。

受限型別

[edit | edit source]

限制類型意味著不允許賦值。 上面描述的“併發型別”始終是受限的。 程式設計師也可以定義自己的型別為受限型別,如下所示

type T is limited …;

(省略號代表private,或對於record 定義,請參閱此頁面上的相應子部分。)受限型別也不具有相等運算子,除非程式設計師定義了相等運算子。

您可以在受限型別章節中瞭解更多資訊。

定義新型別和子型別

[edit | edit source]

您可以使用以下語法定義新型別

type T is...

然後是型別的描述,如每種型別的類別中詳細解釋的那樣。

正式地,上述宣告建立了一個型別及其名為T第一個子型別。 型別本身,正確地稱為“T 的型別”,是匿名的; RM 將其稱為T(用斜體表示),但經常會隨意談論型別 T。 但這是一個學術上的考慮; 對於大多數目的,將T視為一個型別就足夠了。 對於標量型別,還有一個稱為T'Base的基礎型別,它包含 T 的所有值。

對於有符號整數型別,T 的型別包含(完整)數學整數集。 基礎型別是某種硬體型別,圍繞零對稱(可能除了一個額外的負值外),包含 T 的所有值。

如上所述,所有型別都是不相容的; 因此

type Integer_1 is range 1 .. 10;
type Integer_2 is range 1 .. 10;
A : Integer_1 := 8;
B : Integer_2 := A; -- illegal!

是非法的,因為Integer_1Integer_2 是不同的並且不相容的型別。 正是這個特性使編譯器能夠在編譯時檢測邏輯錯誤,例如將檔案描述符新增到位元組數或將長度新增到重量。 這兩個型別具有相同的範圍這一事實並沒有使它們相容:這是名稱等效性的作用,而不是結構等效性。(下面我們將看到如何轉換不相容的型別; 這裡有嚴格的規則。)

建立子型別

[edit | edit source]

您還可以建立給定型別的新子型別,這些子型別將彼此相容,如下所示

type Integer_1 is range 1 .. 10;
subtype Integer_2 is Integer_1      range 7 .. 11;  -- bad
subtype Integer_3 is Integer_1'Base range 7 .. 11;  -- OK
A : Integer_1 := 8;
B : Integer_3 := A; -- OK

Integer_2 的宣告是錯誤的,因為約束7 .. 11Integer_1 不相容; 它在子型別細化時引發Constraint_Error

Integer_1Integer_3 是相容的,因為它們都是相同型別的子型別,即Integer_1'Base

子類型範圍不必重疊,也不必包含在彼此之中。當您將 A 賦值給 B 時,編譯器會在執行時插入範圍檢查;如果此時 A 的值恰好在Integer_3的範圍之外,程式會引發Constraint_Error

有一些預定義的子型別非常有用。

subtype Natural  is Integer range 0 .. Integer'Last;
subtype Positive is Integer range 1 .. Integer'Last;

派生型別

[edit | edit source]

派生型別是從現有型別建立的一種新的完整型別。與其他型別一樣,它與其父型別不相容;但是,它繼承了為父型別定義的原始操作。

type Integer_1 is range 1 .. 10;
type Integer_2 is new Integer_1 range 2 .. 8;
A : Integer_1 := 8;
B : Integer_2 := A; -- illegal!

這裡兩種型別都是離散的;派生型別的範圍必須包含在其父型別的範圍內。將此與子型別進行對比。原因是派生型別繼承了為其父型別定義的原始操作,而這些操作假設了父型別的範圍。下面是此功能的說明:

procedure Derived_Types is

   package Pak is
      type Integer_1 is range 1 .. 10;
      procedure P (I: in Integer_1); -- primitive operation, assumes 1 .. 10
      type Integer_2 is new Integer_1 range 8 .. 10; -- must not break P's assumption
      -- procedure P (I: in Integer_2);  inherited P implicitly defined here
   end Pak;

   package body Pak is
      -- omitted
   end Pak;

   use Pak;
   A: Integer_1 := 4;
   B: Integer_2 := 9;

begin

   P (B); -- OK, call the inherited operation

end Derived_Types;

當我們呼叫P (B)時,引數 B 被轉換為Integer_1;當然,此轉換通過了,因為派生型別(這裡是 8 .. 10)的可接受值的集合必須包含在父型別(1 .. 10)中。然後用轉換後的引數呼叫 P。

但是,考慮上面示例的一個變體:

procedure Derived_Types is

  package Pak is
    type Integer_1 is range 1 .. 10;
    procedure P (I: in Integer_1; J: out Integer_1);
    type Integer_2 is new Integer_1 range 8 .. 10;
  end Pak;

  package body Pak is
    procedure P (I: in Integer_1; J: out Integer_1) is
    begin
      J := I - 1;
    end P;
  end Pak;

  use Pak;

  A: Integer_1 := 4;  X: Integer_1;
  B: Integer_2 := 8;  Y: Integer_2;

begin

  P (A, X);
  P (B, Y);

end Derived_Types;

當呼叫P (B, Y)時,兩個引數都被轉換為Integer_1。因此,對 P 主體中 J (7) 的範圍檢查將透過。但是,在返回引數 Y 時,它會轉換回Integer_2,並且對 Y 的範圍檢查當然會失敗。

考慮到以上內容,您將瞭解到為什麼在以下程式中,Constraint_Error會在執行時呼叫,甚至在呼叫P之前。

procedure Derived_Types is

  package Pak is
    type Integer_1 is range 1 .. 10;
    procedure P (I: in Integer_1; J: out Integer_1);
    type Integer_2 is new Integer_1'Base range 8 .. 12;
  end Pak;

  package body Pak is
    procedure P (I: in Integer_1; J: out Integer_1) is
    begin
      J := I - 1;
    end P;
  end Pak;

  use Pak;

  B: Integer_2 := 11;  Y: Integer_2;

begin

  P (B, Y);

end Derived_Types;

子型別類別

[edit | edit source]

Ada 支援各種具有不同功能的子型別類別。以下是按字母順序排列的概述。

匿名子型別

[edit | edit source]

一個沒有分配名稱的子型別。這種子型別是使用變數宣告建立的:

X : String (1 .. 10) := (others => ' ');

這裡,(1 .. 10) 是約束條件。此變數宣告等效於:

subtype Anonymous_String_Type is String (1 .. 10);

X : Anonymous_String_Type := (others => ' ');

基本型別

[edit | edit source]

在 Ada 中,所有型別都是匿名的,只有子型別可以命名。對於標量型別,匿名型別有一個特殊的子型別,稱為基本型別,它可以使用Subtype'Base符號進行命名。此Name'Attribute(讀作“name tick attribute”)是 Ada 中用於稱為屬性的特殊符號,即由編譯器定義並可以查詢的型別、變數或其他程式實體的特徵。在本例中,基本型別(Subtype'Base)包含第一個子型別的所有值。一些示例:

 type Int is range 0 .. 100;

基本型別Int'Base是由編譯器選擇的硬體型別,它包含Int的值。因此,它的範圍可能是 -27 .. 27-1 或 -215 .. 215-1,或者任何其他此類型別。

 type Enum  is (A, B, C, D);
 type Short is new Enum range A .. C;

Enum'BaseEnum相同,但Short'Base還包含文字D

約束子型別

[edit | edit source]

一個不定子型別的子型別,添加了約束條件。以下示例定義了一個 10 個字元的字串子型別:

 subtype String_10 is String (1 .. 10);

您無法對無約束子型別進行部分約束:

 type My_Array is array (Integer range <>, Integer range <>) of Some_Type;

 --  subtype Constr is My_Array (1 .. 10, Integer range <>);  illegal

 subtype Constr is My_Array (1 .. 10, -100 .. 200);

必須給出所有索引的約束條件,結果必然是一個確定子型別。

確定子型別

[edit | edit source]

確定子型別是指其大小在編譯時已知的子型別。所有不是不定子型別的子型別,根據定義,都是確定子型別。

可以聲明確定子型別的物件,而無需額外的約束條件。

不定子型別

[edit | edit source]

不定子型別是指其大小在編譯時未知,而是在執行時動態計算的子型別。不定子型別本身並不能提供足夠的資訊來建立物件;需要額外的約束條件或顯式初始化表示式才能計算實際大小,從而建立物件。

X : String := "This is a string";

X 是不定(子)型別String的一個物件。它的約束條件隱式地從其初始值派生而來。X 可以更改其值,但不能更改其邊界。

需要注意的是,不必從文字初始化物件。您也可以使用函式。例如:

X : String := Ada.Command_Line.Argument (1);

此語句讀取第一個命令列引數,並將其賦值給X

不定子型別的子型別,如果它沒有新增約束條件,則只會引入原始子型別的新名稱(在不同概念下的重新命名)。

 subtype My_String is String;

My_String以及字串是可互換的。

命名子型別

[edit | edit source]

一個分配了名稱的子型別。“第一個子型別”是使用關鍵字type 建立的(請記住,型別始終是匿名的,型別宣告中的名稱是第一個子型別的名稱),其他子型別則使用關鍵字subtype 建立。例如:

type Count_To_Ten is range 1 .. 10;

Count_to_Ten 是一個合適的整數基本型別的第一個子型別。但是,如果您想將它用作String的索引約束條件,以下宣告是非法的:

subtype Ten_Characters is String (Count_to_Ten);

這是因為String 的索引是Positive,它是Integer 的子型別(這些宣告來自包Standard):

subtype Positive is Integer range 1 .. Integer'Last;

type String is (Positive range <>) of Character;

所以您必須使用以下宣告:

subtype Count_To_Ten is Integer range 1 .. 10;
subtype Ten_Characters is String (Count_to_Ten);

現在,Ten_CharactersString 的那個子型別的名稱,該子型別被約束為Count_To_Ten。您看到對型別和子型別施加約束會產生截然不同的效果。

無約束子型別

[edit | edit source]

任何不定型別也是無約束子型別。但是,無約束和不定並不相同。

 type My_Enum is (A, B, C);
 type My_Record (Discriminant: My_Enum) is ...;

 My_Object_A: My_Record (A);

此型別是無約束和不定的,因為您需要為物件宣告提供實際的辨別式;物件被約束為此辨別式,而此辨別式不能更改。

但是,當為辨別式提供預設值時,型別是確定的,但仍然是無約束的;它允許定義約束物件和無約束物件:

 type My_Enum is (A, B, C);
 type My_Record (Discriminant: My_Enum := A) is ...;

 My_Object_U: My_Record;      --  unconstrained object
 My_Object_B: My_Record (B);  --  constrained to discriminant B like above

這裡,My_Object_U 是無約束的;在宣告時,它具有辨別式 A(預設值),但可以更改。

不相容子型別

[edit | edit source]
 type My_Integer is range -10 .. + 10;
 subtype My_Positive is My_Integer range + 1 .. + 10;
 subtype My_Negative is My_Integer range -10 .. -  1;

這些子型別當然是不相容的。

另一個例子是辨別式記錄的子型別:

 type My_Enum is (A, B, C);
 type My_Record (Discriminant: My_Enum) is ...;
 subtype My_A_Record is My_Record (A);
 subtype My_C_Record is My_Record (C);

這些子型別也是不相容的。

限定表示式

[edit | edit source]

在大多數情況下,編譯器能夠推斷出表示式的型別;例如:

type Enum is (A, B, C);
E : Enum := A;

這裡,編譯器知道A 是型別Enum 的值。但請考慮:

procedure Bad is
   type Enum_1 is (A, B, C);
   procedure P (E : in Enum_1) is... -- omitted
   type Enum_2 is (A, X, Y, Z);
   procedure P (E : in Enum_2) is... -- omitted
begin
   P (A); -- illegal: ambiguous
end Bad;

編譯器無法在兩個版本的P 之間進行選擇;兩者都同樣有效。要消除歧義,您可以使用限定表示式

   P (Enum_1'(A)); -- OK

如以下示例所示,此語法通常用於建立新物件。如果您嘗試編譯此示例,它將失敗,並出現編譯錯誤,因為編譯器將確定 256 不在Byte 的範圍內。

檔案: convert_evaluate_as.adb (檢視, 純文字, 下載頁面, 瀏覽所有)
with Ada.Text_IO;

procedure Convert_Evaluate_As is
   type Byte     is mod 2**8;
   type Byte_Ptr is access Byte;

   package T_IO renames Ada.Text_IO;
   package M_IO is new Ada.Text_IO.Modular_IO (Byte);

   A : constant Byte_Ptr := new Byte'(256);
begin
   T_IO.Put ("A = ");
   M_IO.Put (Item  => A.all,
             Width =>  5,
             Base  => 10);
end Convert_Evaluate_As;

獲取字串字面量的長度時,應使用限定表示式。

"foo"'Length                  {{Ada/--| compilation error: prefix of attribute must be a name}}
                              {{Ada/--|                    qualify expression to turn it into a name}}
String'("foo" & "bar")'Length {{Ada/--| 6}}

型別轉換

[編輯 | 編輯原始碼]

資料並不總是以您需要的格式出現。因此,您必須面對轉換它們的任務。作為一門真正通用的語言,尤其側重於“任務關鍵型”、“系統程式設計”和“安全”,Ada 提供了幾種轉換技術。最難的部分是選擇正確的技術,因此以下列表按實用性排序。您應該首先嚐試第一個;最後一個技術是最後的手段,如果其他所有技術都失敗,才使用它。還有一些相關技術,您可以選擇使用它們而不是實際轉換資料。

由於最重要的方面不是成功轉換的結果,而是系統將如何對無效轉換做出反應,因此所有示例也演示了錯誤的轉換。

顯式型別轉換

[編輯 | 編輯原始碼]

顯式型別轉換看起來很像函式呼叫;它不使用撇號(撇號,')像限定表示式那樣。

Type_Name (Expression)

編譯器首先檢查轉換是否合法,如果是,它會在轉換點插入一個執行時檢查;因此名稱為檢查轉換。如果轉換失敗,程式將引發 Constraint_Error。大多數編譯器非常聰明,會最佳化掉約束檢查;因此,您不必擔心任何效能損失。一些編譯器還可以警告說約束檢查將始終失敗(並用無條件引發來最佳化檢查)。

顯式型別轉換是合法的

  • 在任何兩種數值型別之間
  • 在同一型別的任何兩個子型別之間
  • 在從同一型別派生的任何兩種型別之間(注意標記型別的特殊規則)
  • 在滿足某些條件下的陣列型別之間(參見 RM 4.6(24.2/2..24.7/2))
  • 以及其他任何地方

(使用類寬和匿名訪問型別時,規則會變得更加複雜。)

I: Integer := Integer (10);  -- Unnecessary explicit type conversion
J: Integer := 10;            -- Implicit conversion from universal integer
K: Integer := Integer'(10);  -- Use the value 10 of type Integer: qualified expression
                             -- (qualification not necessary here).

此示例說明了顯式型別轉換

檔案: convert_checked.adb (檢視, 純文字, 下載頁面, 瀏覽所有)
with Ada.Text_IO;

procedure Convert_Checked is
   type Short is range -128 .. +127;
   type Byte  is mod 256;

   package T_IO renames Ada.Text_IO;
   package I_IO is new Ada.Text_IO.Integer_IO (Short);
   package M_IO is new Ada.Text_IO.Modular_IO (Byte);

   A : Short := -1;
   B : Byte;
begin
   B := Byte (A);  --  range check will lead to Constraint_Error
   T_IO.Put ("A = ");
   I_IO.Put (Item  =>  A,
             Width =>  5,
             Base  => 10);
   T_IO.Put (", B = ");
   M_IO.Put (Item  =>  B,
             Width =>  5,
             Base  => 10);
end Convert_Checked;

在任何兩種數值型別之間都可以進行顯式轉換:整數、定點和浮點型別。如果涉及的型別之一是定點或浮點型別,編譯器不僅檢查範圍約束(因此上面的程式碼將引發 Constraint_Error),而且還會執行任何必要的精度損失。

示例 1:精度損失導致過程始終只打印“0”或“1”,因為P / 100是整數,始終為零或一。

with Ada.Text_IO;
procedure Naive_Explicit_Conversion is
   type Proportion is digits 4 range 0.0 .. 1.0;
   type Percentage is range 0 .. 100;
   function To_Proportion (P : in Percentage) return Proportion is
   begin
      return Proportion (P / 100);
   end To_Proportion;
begin
   Ada.Text_IO.Put_Line (Proportion'Image (To_Proportion (27)));
end Naive_Explicit_Conversion;

示例 2:我們使用中間浮點型別來保證精度。

with Ada.Text_IO;
procedure Explicit_Conversion is
   type Proportion is digits 4 range 0.0 .. 1.0;
   type Percentage is range 0 .. 100;
   function To_Proportion (P : in Percentage) return Proportion is
      type Prop is digits 4 range 0.0 .. 100.0;
   begin
      return Proportion (Prop (P) / 100.0);
   end To_Proportion;
begin
   Ada.Text_IO.Put_Line (Proportion'Image (To_Proportion (27)));
end Explicit_Conversion;

您可能想知道為什麼要在同一型別的兩個子型別之間進行轉換。一個例子將說明這一點。

subtype String_10 is String (1 .. 10);
X: String := "A line long enough to make the example valid";
Slice: constant String := String_10 (X (11 .. 20));

這裡,Slice 的邊界為 1 和 10,而X (11 .. 20) 的邊界為 11 和 20。

表示更改

[編輯 | 編輯原始碼]

型別轉換可用於記錄或陣列的打包和解包。

type Unpacked is record
  -- any components
end record;

type Packed is new Unpacked;
for  Packed use record
  -- component clauses for some or for all components
end record;
P: Packed;
U: Unpacked;

P := Packed (U);  -- packs U
U := Unpacked (P);  -- unpacks P

非數值型別的檢查轉換

[編輯 | 編輯原始碼]

上面的示例都圍繞數值型別之間的轉換;可以透過這種方式在任何兩種數值型別之間進行轉換。但是,在非數值型別之間(例如,在陣列型別或記錄型別之間)會發生什麼?答案是雙重的

  • 您可以在型別與其派生型別之間進行顯式轉換,或者在從同一型別派生的型別之間進行轉換,
  • 僅此而已。沒有其他轉換是可能的。

為什麼要從另一個記錄型別派生一個記錄型別?因為表示子句。在這裡,我們進入了低階系統程式設計的領域,這對於膽小者來說並不適合,也不適合桌面應用程式。因此,請堅持住,讓我們深入研究。

假設您有一個記錄型別,它使用預設的有效表示。現在,您想將此記錄寫入裝置,該裝置使用特殊的記錄格式。這種特殊表示更緊湊(使用更少的位),但效率極低。您想要有一個分層程式設計介面:面向應用程式的上層使用有效表示。下層是一個裝置驅動程式,它直接訪問硬體並使用低效表示。

package Device_Driver is
   type Size_Type is range 0 .. 64;
   type Register is record
      A, B : Boolean;
      Size : Size_Type;
   end record;

   procedure Read (R : out Register);
   procedure Write (R : in Register);
end Device_Driver;

編譯器為Register 選擇了一個預設的、有效的表示。例如,在 32 位機器上,它可能會使用三個 32 位字,一個用於 A,一個用於 B,一個用於 Size。這種有效表示對於應用程式來說很好,但在某一點,我們希望將整個記錄轉換為僅 8 位,因為這就是我們的硬體所需的。

package body Device_Driver is
   type Hardware_Register is new Register; -- Derived type.
   for Hardware_Register use record
      A at 0 range 0 .. 0;
      B at 0 range 1 .. 1;
      Size at 0 range 2 .. 7;
   end record;

   function Get return Hardware_Register; -- Body omitted
   procedure Put (H : in Hardware_Register); -- Body omitted

   procedure Read (R : out Register) is
      H : Hardware_Register := Get;
   begin
      R := Register (H); -- Explicit conversion.
   end Read;

   procedure Write (R : in Register) is
   begin
      Put (Hardware_Register (R)); -- Explicit conversion.
   end Write;
end Device_Driver;

在上面的示例中,包主體聲明瞭一個具有低效但緊湊表示的派生型別,並轉換到該型別和從該型別轉換。

這說明了型別轉換會導致表示更改

面向物件程式設計中的檢視轉換

[編輯 | 編輯原始碼]

面向物件程式設計中,您必須區分特定型別和類寬型別。

對於特定型別,只有朝根方向的轉換是可能的,當然不會失敗。沒有反方向的轉換(從哪裡獲取進一步的元件?)擴充套件聚合必須使用。

對於轉換本身,源物件中不存在於目標物件中的任何元件都不會丟失,它們只是隱藏了。因此,這種轉換被稱為檢視轉換,因為它提供了一個作為目標型別物件的源物件的檢視(尤其它不會更改物件的標記)。

在面向物件程式設計中,為檢視轉換的結果重新命名是一種常見的習慣用法。(重新命名宣告不會建立新的物件;它只是為已經存在的東西提供一個新名稱。)

type Parent_Type is tagged record
   <components>;
end record;
type Child_Type is new Parent_Type with record
   <further components>;
end record;

Child_Instance : Child_Type;
Parent_View    : Parent_Type renames Parent_Type (Child_Instance);
Parent_Part    : Parent_Type := Parent_Type (Child_Instance);

Parent_View 不是一個新物件,而是Child_Instance 作為父級物件的另一個名稱,即只有父級元件可見,子級特定元件被隱藏。但是,Parent_Part 是父級型別的一個物件,當然它沒有用於子級特定元件的儲存空間,因此它們在賦值時會丟失。

從標記型別T派生的所有型別形成一個以T為根的樹。類寬型別T'Class 可以儲存此樹中的任何物件。對於類寬型別,可以進行任何方向的轉換;有一個執行時標記檢查,如果檢查失敗,它會引發Constraint_Error。這些轉換也是檢視轉換,沒有建立或丟失資料。

Object_1 : Parent_Type'Class := Parent_Type'Class (Child_Instance);
Object_2 : Parent_Type'Class renames Parent_Type'Class (Child_Instance);

Object_1 是一個新物件,一個副本;Object_2 只是一個新名稱。這兩個物件都是類寬型別。轉換為給定類中的任何型別都是合法的,但會進行標記檢查。

Success : Child_Type := Child_Type (Parent_Type'Class (Parent_View));
Failure : Child_Type := Child_Type (Parent_Type'Class (Parent_Part));

第一次轉換通過了標記檢查,並且Child_InstanceSuccess 兩個物件都相等。第二次轉換未透過標記檢查。(這種轉換賦值很少使用;排程會自動執行此操作,請參見面向物件程式設計。)

您可以使用成員資格測試自行執行這些檢查

if Parent_View in Child_Type then ...
if Parent_View in Child_Type'Class then ...

還有包Ada.Tags

地址轉換

[編輯 | 編輯原始碼]

Ada 的訪問型別不僅僅是一個記憶體地址(一個薄指標)。根據實現和使用的訪問型別訪問可能會保留其他資訊(一個胖指標)。例如,GNAT 為每個訪問一個無限物件保留兩個記憶體地址——一個用於資料,另一個用於約束資訊(' Size ', ' First ', ' Last ')

如果您想將一個訪問轉換為一個簡單的記憶體位置,您可以使用包 System.Address_To_Access_Conversions。但是請注意,地址和胖指標不能相互轉換。

陣列物件的地址是其第一個元件的地址。因此,邊界在這樣的轉換中會丟失。

type My_Array is array (Positive range <>) of Something;
A: My_Array (50 .. 100);

     A'Address = A(A'First)'Address

未經檢查的轉換

[edit | edit source]

Pascal 的一個主要批評是“沒有逃脫”。原因是,有時您必須轉換不相容的型別。為此,Ada 提供了泛型函式 Unchecked_Conversion

generic
   type Source (<>) is limited private;
   type Target (<>) is limited private;
function Ada.Unchecked_Conversion (S : Source) return Target;

Unchecked_Conversion 將按位複製源資料,並在目標型別下重新解釋它們,而無需任何檢查。確保滿足 RM 中所述的關於未經檢查的轉換的要求是您的責任 13.9 (Annotated);否則,結果將取決於實現,甚至可能導致異常資料。在有問題的案例中,使用 'Valid 屬性在轉換後檢查資料的有效性。

對(一個例項)Unchecked_Conversion 的函式呼叫將複製源資料到目標。編譯器也可以就地執行轉換(每個例項都有約定 Intrinsic)。

要使用 Unchecked_Conversion,您需要例項化泛型。

在下面的示例中,您可以看到它是如何完成的。執行時,該示例將輸出 A = -1, B = 255。不會報告錯誤,但這是您期望的結果嗎?

檔案:convert_unchecked.adb (view, plain text, download page, browse all)
with Ada.Text_IO;
with Ada.Unchecked_Conversion;

procedure Convert_Unchecked is

   type Short is range -128 .. +127;
   type Byte  is mod 256;

   package T_IO renames Ada.Text_IO;
   package I_IO is new Ada.Text_IO.Integer_IO (Short);
   package M_IO is new Ada.Text_IO.Modular_IO (Byte);

   function Convert is new Ada.Unchecked_Conversion (Source => Short,
                                                     Target => Byte);

   A : constant Short := -1;
   B : Byte;

begin

   B := Convert (A);
   T_IO.Put ("A = ");
   I_IO.Put (Item  =>  A,
             Width =>  5,
             Base  => 10);
   T_IO.Put (", B = ");
   M_IO.Put (Item  =>  B,
             Width =>  5,
             Base  => 10);

end Convert_Unchecked;

當然,在賦值 B := Convert (A); 中有一個範圍檢查。因此,如果 B 被定義為 B: Byte range 0 .. 10;,則會引發 Constraint_Error

覆蓋

[edit | edit source]

如果複製 Unchecked_Conversion 的結果在效能方面過於浪費,那麼您可以嘗試覆蓋,即地址對映。透過使用覆蓋,兩個物件共享同一個記憶體位置。如果您為其中一個賦值,另一個也會隨之改變。語法是

for Target'Address use expression;
pragma Import (Ada, Target);

其中 expression 定義源物件的地址。

雖然覆蓋看起來比 Unchecked_Conversion 更優雅,但您應該意識到它們更危險,並且更可能做錯事。例如,如果 Source'Size < Target'Size,並且您為 Target 賦值,您可能會無意中寫入分配給另一個物件的記憶體。

您還必須注意目標型別的物件的隱式初始化,因為它們會覆蓋源物件的實際值。Import pragma with convention Ada 可以用於防止這種情況,因為它會避免隱式初始化,RM B.1 (Annotated)

下面的示例與“未經檢查的轉換”部分的示例相同。

檔案:convert_address_mapping.adb (view, plain text, download page, browse all)
with Ada.Text_IO;

procedure Convert_Address_Mapping is
   type Short is range -128 .. +127;
   type Byte  is mod 256;

   package T_IO renames Ada.Text_IO;
   package I_IO is new Ada.Text_IO.Integer_IO (Short);
   package M_IO is new Ada.Text_IO.Modular_IO (Byte);

   A : aliased Short;
   B : aliased Byte;
  
   for B'Address use A'Address;
  pragma Import (Ada, B);
  
begin
   A := -1;
   T_IO.Put ("A = ");
   I_IO.Put (Item  =>  A,
             Width =>  5,
             Base  => 10);
   T_IO.Put (", B = ");
   M_IO.Put (Item  =>  B,
             Width =>  5,
             Base  => 10);
end Convert_Address_Mapping;

匯出/匯入

[edit | edit source]

僅供記錄:還有另一種方法使用ExportImport 編譯指示。但是,由於這種方法比覆蓋更徹底地破壞了 Ada 的可見性和型別概念,因此在語言介紹中沒有它的一席之地,並留給專家處理。

有符號整數型別的型別詳細討論

[edit | edit source]

如前所述,型別宣告

type T is range 1 .. 10;

宣告一個匿名型別 T 及其第一個子型別 T(請注意斜體字)。T 包含所有數學整數的完整集合。靜態表示式和命名數字使用此事實。

所有數字整數字面量都屬於 Universal_Integer 型別。它們在需要時被轉換為相應的特定型別。Universal_Integer 本身沒有運算子。

一些使用靜態命名數字的示例

 S1: constant := Integer'Last + Integer'Last;       -- "+" of Integer
 S2: constant := Long_Integer'Last + 1;             -- "+" of Long_Integer
 S3: constant := S1 + S2;                           -- "+" of root_integer
 S4: constant := Integer'Last + Long_Integer'Last;  -- illegal

靜態表示式在編譯時在相應的型別上進行評估,不進行溢位檢查,即數學精確(僅受計算機儲存限制)。然後結果隱式轉換為 Universal_Integer

S2 中的字面量 1 屬於 Universal_Integer 型別,並隱式轉換為 Long_Integer

S3 隱式將加數轉換為 root_integer,執行計算並轉換回 Universal_Integer

S4 非法,因為它混合了兩種不同的型別。但是,您可以將其寫成

 S5: constant := Integer'Pos (Integer'Last) + Long_Integer'Pos (Long_Integer'Last);  -- "+" of root_integer

其中 Pos 屬性將值轉換為 Universal_Integer,然後進一步隱式轉換為 root_integer,相加並將結果轉換回 Universal_Integer

root_integer 是硬體可以表示的匿名最大整數型別。它的範圍是 System.Min_Integer .. System.Max_Integer。所有整數型別都源於 root_integer,即從它派生而來。Universal_Integer 可以被視為 root_integer'Class

在執行時,當然在相應的子型別上執行計算,包括範圍檢查和溢位檢查。但是,中間結果可能會超出範圍限制。因此,對於上面的子型別 T 中的 I, J, K,以下程式碼將返回正確的結果

I := 10;
J :=  8;
K := (I + J) - 12;
-- I := I + J;  --  range check would fail, leading to Constraint_Error

實數字面量屬於 Universal_Real 型別,並且相應的規則也適用。

型別之間的關係

[edit | edit source]

型別可以從其他型別建立。例如,陣列型別由兩種型別組成,一種用於陣列的索引,另一種用於陣列的元件。然後,陣列表示一種關聯,即索引型別的一個值和元件型別的一個值之間的關聯。

 type Color is (Red, Green, Blue);
 type Intensity is range 0 .. 255;
 
 type Colored_Point is array (Color) of Intensity;

型別Color是索引型別,型別Intensity是陣列型別 Colored_Point 的元件型別。Colored_Point. 見 array

另見

[edit | edit source]

華夏公益教科書

[edit | edit source]

Ada 參考手冊

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