跳轉到內容

Ada 程式設計/型別系統

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

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

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

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

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

預定義型別

[編輯 | 編輯原始碼]

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

這些型別在 Standard 包中預定義

整數
此型別至少涵蓋範圍 .. (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

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 語言的讀者可以參考的 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 …;

(省略號表示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 的範圍內。

File: convert_evaluate_as.adb (view, plain text, download page, browse all)
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]

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

Type_Name (Expression)

編譯器首先檢查轉換是否合法,如果合法,它會在轉換點插入一個執行時檢查;因此稱為checked conversion。如果轉換失敗,程式將引發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 (view, plain text, download page, browse all)
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 (檢視, 純文字, 下載頁面, 瀏覽所有)
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 錯誤。

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

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

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

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

你還要注意目標型別物件的隱式初始化,因為它們會覆蓋源物件的實際值。可以使用帶 Ada 約定的 Import 預編譯指令來防止這種情況,因為它避免了隱式初始化,RM B.1 (帶註釋的)

下面的示例與“Unchecked Conversion”中的示例效果相同。

檔案: convert_address_mapping.adb (檢視, 純文字, 下載頁面, 瀏覽所有)
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;

匯出 / 匯入

[編輯 | 編輯原始碼]

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

關於有符號整型型別的詳細討論

[編輯 | 編輯原始碼]

如前所述,型別宣告

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 型別,適當地應用與上述類似的規則。

型別之間的關係

[編輯 | 編輯原始碼]

型別可以由其他型別構成。例如,陣列型別由兩個型別構成,一個是陣列的索引型別,另一個是陣列的元素型別。然後,陣列表示一個關聯,即索引型別的一個值與元素型別的一個值之間的關聯。

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

型別Color是索引型別,型別Intensity是陣列型別的元素型別Colored_Point. 請參見 陣列

另請參見

[編輯 | 編輯原始碼]

華夏公益教科書

[編輯 | 編輯原始碼]

Ada 參考手冊

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