跳轉到內容

Ada 程式設計/型別系統

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

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

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

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

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

預定義型別

[編輯 | 編輯原始碼]

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

這些型別在 Standard 包中預定義

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

SystemSystem.Storage_Elements預定義了一些型別,這些型別主要用於低階程式設計和與硬體的介面。

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 的型別可以按如下方式分類。

具體型別 vs. 類範圍型別

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

T'ClassT'Class'Class 相同。

具有具體型別引數的原始操作是非排程的,那些具有類範圍型別引數的操作是排程的。

可以透過從具體型別派生來宣告新型別;原始操作是透過派生來繼承的。不能從類範圍型別派生。

受限型別 vs. 無限型別

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 會分配最大大小,以便大小變化不會給存在判別式變化帶來的問題帶來任何問題。另一種可能性是在堆上進行隱式動態分配,並在大小發生變化時重新分配,然後釋放。

確定型別 vs. 不確定型別

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

無限型別 vs. 不確定型別

請注意,無限子型別不一定是無限的,如上所述,RD 是一個確定無限子型別。

併發型別

[edit | edit source]

Ada 語言除了將型別用於分類資料和操作之外,還將型別用於另一個目的。型別系統整合了併發性(執行緒、並行)。程式設計師將使用型別來表達其程式的併發控制執行緒。

該型別系統這部分的核心部分,任務型別和保護型別在關於任務的部分中進行了更深入的解釋。

受限型別

[edit | edit source]

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

type T is limited …;

(省略號代表privaterecord 定義,請參見本頁中相應的子部分。)受限型別也沒有相等運算子,除非程式設計師定義一個。

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

定義新的型別和子型別

[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是不同的、不相容的型別。正是這個特性使編譯器能夠在編譯時檢測到邏輯錯誤,例如將檔案描述符新增到位元組數或將長度新增到重量。這兩個型別具有相同的範圍這一事實並不能使它們相容:這是名稱等價起作用,而不是結構等價。(下面,我們將看到如何將不相容的型別相互轉換;對此有嚴格的規則。)

建立子型別

[編輯 | 編輯原始碼]

您也可以建立給定型別的新的子型別,這些子型別將彼此相容,例如

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;

派生型別

[編輯 | 編輯原始碼]

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

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 的範圍檢查當然會失敗。

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

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;

子型別類別

[編輯 | 編輯原始碼]

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

匿名子型別

[編輯 | 編輯原始碼]

沒有分配名稱的子型別。這種子型別是透過變數宣告建立的

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

這裡,(1 .. 10) 是約束。這個變數宣告等同於

subtype Anonymous_String_Type is String (1 .. 10);

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

基本型別

[編輯 | 編輯原始碼]

在 Ada 中,所有型別都是匿名的,只有子型別可以是命名的。對於標量型別,匿名型別的特殊子型別稱為基本型別,它可以使用Subtype'Base符號命名。這個Name'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

約束子型別

[編輯 | 編輯原始碼]

新增約束的無限子型別的子型別。以下示例定義了一個 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);

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

明確子型別

[編輯 | 編輯原始碼]

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

可以宣告明確子型別的物件,而無需額外的約束。

無限子型別

[編輯 | 編輯原始碼]

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

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

X 是無限(子)型別String的物件。它的約束隱式地從其初始值派生。X 可以改變它的值,但不能改變它的邊界。

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

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

此語句讀取第一個命令列引數並將其分配給X

無限子型別的子型別,如果它不新增約束,只會為原始子型別引入一個新名稱(在不同的概念下的一種重新命名)。

 subtype My_String is String;

My_StringString是可互換的。

命名子型別

[編輯 | 編輯原始碼]

分配了名稱的子型別。“第一個子型別”是使用關鍵字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。您會發現,對型別施加約束與對子型別施加約束的效果截然不同。

無約束子型別

[編輯 | 編輯原始碼]

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

 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(預設值),但此值可能會更改。

不相容子型別

[編輯 | 編輯原始碼]
 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;

這裡編譯器知道 AEnum 型別的值。但考慮

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}}

型別轉換

[edit | edit source]

資料並不總是以您需要的格式出現。因此,您必須面對將它們轉換的任務。作為一種真正的多用途語言,特別強調“任務關鍵型”、“系統程式設計”和“安全性”,Ada 有幾種轉換技術。最困難的部分是選擇合適的技術,因此以下列表按實用性排序。您應該首先嚐試第一個;最後一個技術是最後的手段,如果所有其他方法都失敗了,則使用它。還有一些相關技術,您可能會選擇它們來代替實際轉換資料。

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

顯式型別轉換

[edit | edit source]

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

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。

表示形式的改變

[edit | edit source]

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

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

非數字型別的檢查轉換

[edit | edit source]

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

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

為什麼要從另一個記錄型別派生記錄型別?因為表示子句。這裡我們進入低階系統程式設計領域,這既不適合膽小的人,也不適用於桌面應用程式。所以抓緊,讓我們深入瞭解。

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

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;

在上面的示例中,包體聲明瞭一個具有無效但緊湊表示的派生型別,並進行轉換。

這說明了 **型別轉換會導致表示形式的改變**。

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

[edit | edit source]

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

對於特定型別,只有向根方向的轉換是可能的,這當然不會失敗。沒有相反方向的轉換 (您將從哪裡獲得更遠的元件?);相反,應該使用 *擴充套件聚合*。

對於轉換本身,源物件中不在目標物件中的任何元件都不會丟失,它們只是從可見性中隱藏起來。因此,這種轉換被稱為 *檢視轉換*,因為它提供了將源物件視為目標型別物件的檢視(特別是它不會更改物件的標記)。

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

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

地址轉換

[edit | edit source]

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 pragma。但是,由於這種方法比覆蓋更徹底地破壞了 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

在執行時,計算當然是在適當的子型別上進行範圍檢查和溢位檢查。但是中間結果可能會超過範圍限制。因此,對於上述子型別 TIJK,以下程式碼將返回正確的結果

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的元件型別。參見 array

另請參見

[edit | edit source]

華夏公益教科書

[edit | edit source]

Ada 參考手冊

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