Ada 程式設計/型別系統
Ada 的型別系統允許程式設計師構建強大的抽象來代表現實世界,並向編譯器提供有價值的資訊,以便編譯器可以在邏輯或設計錯誤成為 bug 之前找到它們。它是語言的核心,優秀的 Ada 程式設計師學會利用它來獲得巨大優勢。四個原則支配著型別系統
- 型別:一種對資料進行分類的方式。字元是 'a' 到 'z' 的型別。整數是包含 0,1,2.... 的型別。
- 強型別:型別彼此不相容,因此不可能混合蘋果和橙子。編譯器不會猜測你的蘋果是橙子。你必須明確地說 my_fruit = fruit(my_apple)。強型別減少了錯誤的數量。這是因為開發人員可以很容易地將浮點數寫入整數變數而不知情。現在,你需要的程式才能成功執行的資料在編譯器轉換型別時丟失了。Ada 會生氣並拒絕開發人員的愚蠢錯誤,拒絕進行轉換,除非明確告知。
- 靜態型別:在編譯時進行型別檢查,這使得可以在早期發現型別錯誤。
- 抽象:型別代表現實世界或待解決的問題;而不是計算機如何內部表示資料。有一些方法可以指定型別必須如何在位級別表示,但我們將把這個討論留到下一章。抽象的例子是你的汽車。你並不真正知道它是如何工作的,你只知道它是一堆笨拙的金屬在移動。你使用的幾乎所有技術都是抽象的層,以簡化構成它的複雜電路 - 軟體也是如此。你想要抽象,因為類中的程式碼比除錯時沒有解釋的 100 個 if 語句更有意義
- 名稱等價:與大多數其他語言中使用的結構等價相反。兩種型別只有在名稱相同的情況下才相容;不是如果它們恰好具有相同的大小或位表示。因此,你可以宣告兩個具有相同範圍但完全不相容的整數型別,或者兩個具有完全相同元件但彼此不相容的記錄型別。
型別彼此不相容。但是,每個型別都可以具有任意數量的子型別,這些子型別與其基本型別相容,並且可能彼此相容。請參見下面的子型別示例,它們彼此不相容。
有幾種預定義型別,但大多數程式設計師更喜歡定義自己的特定於應用程式的型別。但是,這些預定義型別作為獨立開發的庫之間的介面非常有用。預定義庫顯然也使用這些型別。
這些型別在 Standard 包中預定義
- 整數
- 此型別至少涵蓋範圍 .. (RM 3.5.4:(21) [註釋])。標準還定義了此型別的
Natural和Positive子型別。

- 浮點數
- 此型別只有非常弱的實現要求(RM 3.5.7:(14) [註釋]);大多數情況下,你將定義自己的浮點型別,並指定精度和範圍要求。
- 持續時間
- 用於計時的一種定點型別。它以秒為單位表示一段時間(RM A.1:(43) [註釋])。
- 字元
- 一種特殊形式的列舉。有三種預定義的字元型別:8 位字元(稱為
Character)、16 位字元(稱為Wide_Character)和 32 位字元(Wide_Wide_Character)。Character從語言的第一個版本(Ada 83)開始就存在,Wide_Character在Ada 95中新增,而Wide_Wide_Character型別在Ada 2005 中可用。 - 字串
- 三種不定陣列型別,分別為
Character、Wide_Character和Wide_Wide_Character。標準庫包含用於處理三種變體的字串的包:固定長度(Ada.Strings.Fixed)、長度可變,但低於某個上限(Ada.Strings.Bounded)和無界長度(Ada.Strings.Unbounded)。這些包中的每一個都有一個Wide_和一個Wide_Wide_變體。 - 布林值
- Ada 中的
Boolean是一個具有特殊語義的列舉,包含False和True。
包 System 和 System.Storage_Elements 預定義了一些主要用於低階程式設計和硬體介面的型別。
- System.Address
- 記憶體中的地址。
- System.Storage_Elements.Storage_Offset
- 偏移量,可以將其加到地址上以獲得新的地址。 也可以從一個地址中減去另一個地址來獲得它們之間的偏移量。 總之,
Address、Storage_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]型別按層次結構組織。 一種型別會從層次結構中高於它的型別繼承屬性。 例如,所有標量型別(整數、列舉、模、定點和浮點型別)都具有 運算子 "<"、">" 和為它們定義的算術運算子,所有離散型別都可以用作陣列索引。

以下是每類型別的概覽;請點選連結以獲取詳細說明。 括號中列出了熟悉 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. 類範圍型別
typeTis... -- a specific type T'Class -- the corresponding class-wide type (exists only for tagged types)
具有特定型別引數的原始操作是非排程的,而具有類範圍型別引數的原始操作是排程的。
可以透過從特定型別派生來宣告新型別;原始操作透過派生來繼承。 不能從類範圍型別派生。
約束型別 vs. 無約束型別
typeIisrange1 .. 10; -- constrainedtypeACisarray(1 .. 10)of... -- constrained
typeAUisarray(Irange<>)of... -- unconstrainedtypeR (X: Discriminant [:= Default])is... -- unconstrained
透過對無約束子型別進行約束,子型別或物件會變為約束型別。
subtypeRCisR (Value); -- constrained subtype of R OC: R (Value); -- constrained object of anonymous constrained subtype of R OU: R; -- unconstrained object
僅當在上面的型別宣告中給出預設值時,才有可能宣告無約束物件。 語言沒有指定這些物件是如何分配的。 GNAT 會分配最大大小,以便大小的更改(可能在判別式更改時出現)不會出現問題。 另一種可能性是在堆上進行隱式動態分配,並在大小更改時進行重新分配,然後進行釋放。
確定性型別 vs. 不確定性型別
typeIisrange1 .. 10; -- definitetypeRD (X: Discriminant := Default)is... -- definite
typeT (<>)is... -- indefinitetypeAUisarray(Irange<>)of... -- indefinitetypeRI (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]限制類型意味著不允許賦值。 上述的“併發型別”始終是受限的。 程式設計師也可以像這樣定義他們自己的受限型別:
typeTislimited…;
(省略號表示private,或者表示record 定義,請參閱本頁面上的相應小節。)
您可以在 受限型別 章節中瞭解更多資訊。
定義新型別和子型別
[edit | edit source]您可以使用以下語法定義新型別:
typeTis...
然後按照每種型別類別中的詳細說明來描述型別。
從形式上講,上面的宣告建立了一個型別及其第一個子型別,名為 T。 該型別本身,正確地稱為“T 的型別”,是匿名的;RM 將其稱為 T(用斜體表示),但通常會不嚴謹地談論型別 T。 但這是一個學術上的考慮;對於大多數目的,將 T 視為一種型別就足夠了。 對於標量型別,還有一個稱為 T'Base 的基本型別,它包含 T 的所有值。
對於有符號整數型別,T 的型別包含(完整)一組數學整數。 基本型別是一種特定的硬體型別,關於零對稱(可能除一個額外的負值外),包含 T 的所有值。
如上所述,所有型別都是不相容的;因此
typeInteger_1isrange1 .. 10;typeInteger_2isrange1 .. 10; A : Integer_1 := 8; B : Integer_2 := A; -- illegal!
是非法的,因為 Integer_1 和 Integer_2 是不同的且不相容的型別。 正是由於這個特性,編譯器才能在編譯時檢測到邏輯錯誤,例如將檔案描述符新增到位元組數或將長度新增到重量。 這兩種型別具有相同範圍這一事實並不能使它們相容:這是名稱等效性在起作用,而不是結構等效性。(下面,我們將看到如何對不相容型別進行轉換;這方面有嚴格的規則。)
建立子型別
[edit | edit source]您還可以建立給定型別的子型別,這些子型別彼此相容,如下所示:
typeInteger_1isrange1 .. 10;subtypeInteger_2isInteger_1range7 .. 11; -- badsubtypeInteger_3isInteger_1'Baserange7 .. 11; -- OK A : Integer_1 := 8; B : Integer_3 := A; -- OK
Integer_2 的宣告是錯誤的,因為約束 7 .. 11 與 Integer_1 不相容;它會在子型別細化時引發 Constraint_Error。
Integer_1 和 Integer_3 相容,因為它們都是同一型別的子型別,即 Integer_1'Base。
子類型範圍不必重疊或彼此包含。 當您將 A 賦值給 B 時,編譯器會插入執行時範圍檢查;如果 A 的值(此時)恰好位於 Integer_3 的範圍之外,程式會引發 Constraint_Error。
有一些非常有用的預定義子型別:
subtypeNaturalisIntegerrange0 .. Integer'Last;subtypePositiveisIntegerrange1 .. Integer'Last;
派生型別
[edit | edit source]派生型別是從現有型別建立的一種全新的完整型別。 與任何其他型別一樣,它與其父型別不相容;但是,它繼承了為父型別定義的原始操作。
typeInteger_1isrange1 .. 10;typeInteger_2isnewInteger_1range2 .. 8; A : Integer_1 := 8; B : Integer_2 := A; -- illegal!
這裡,兩種型別都是離散的;派生型別的範圍必須包含在其父型別的範圍內。 將此與子型別進行對比。 原因是派生型別繼承了為其父型別定義的原始操作,而這些操作假設了父型別的範圍。 以下是如何演示此功能:
procedureDerived_TypesispackagePakistypeInteger_1isrange1 .. 10;procedureP (I:inInteger_1); -- primitive operation, assumes 1 .. 10typeInteger_2isnewInteger_1range8 .. 10; -- must not break P's assumption -- procedure P (I: in Integer_2); inherited P implicitly defined hereendPak;packagebodyPakis-- omittedendPak;usePak; A: Integer_1 := 4; B: Integer_2 := 9;beginP (B); -- OK, call the inherited operationendDerived_Types;
當我們呼叫P (B)時,引數B將被轉換為Integer_1;這種轉換當然會透過,因為派生型別(這裡為8 .. 10)的可接受值集合必須包含在父型別(1 .. 10)的值集合中。然後P被呼叫,並帶有轉換後的引數。
但是,考慮上面例子的一個變體
procedureDerived_TypesispackagePakistypeInteger_1isrange1 .. 10;procedureP (I:inInteger_1; J:outInteger_1);typeInteger_2isnewInteger_1range8 .. 10;endPak;packagebodyPakisprocedureP (I:inInteger_1; J:outInteger_1)isbeginJ := I - 1;endP;endPak;usePak; A: Integer_1 := 4; X: Integer_1; B: Integer_2 := 8; Y: Integer_2;beginP (A, X); P (B, Y);endDerived_Types;
當呼叫P (B, Y)時,兩個引數都被轉換為Integer_1。因此,P主體中對J(7)的範圍檢查將透過。但是,在返回值引數Y被轉換回Integer_2時,對Y的範圍檢查當然會失敗。
考慮到以上情況,您將明白為什麼在以下程式中,Constraint_Error將在執行時被呼叫,甚至在呼叫P之前。
procedureDerived_TypesispackagePakistypeInteger_1isrange1 .. 10;procedureP (I:inInteger_1; J:outInteger_1);typeInteger_2isnewInteger_1'Baserange8 .. 12;endPak;packagebodyPakisprocedureP (I:inInteger_1; J:outInteger_1)isbeginJ := I - 1;endP;endPak;usePak; B: Integer_2 := 11; Y: Integer_2;beginP (B, Y);endDerived_Types;
子型別類別
[edit | edit source]Ada支援各種子型別類別,它們具有不同的能力。以下是按字母順序排列的概述。
匿名子型別
[edit | edit source]沒有分配名稱的子型別。這種子型別是透過變數宣告建立的
X : String (1 .. 10) := (others => ' ');
這裡,(1 .. 10) 是約束。此變數宣告等效於
subtypeAnonymous_String_TypeisString (1 .. 10); X : Anonymous_String_Type := (others=> ' ');
基本型別
[edit | edit source]在 Ada 中,所有型別都是匿名 的,只有子型別可以命名。對於標量型別,匿名型別有一個特殊的子型別,稱為基本型別,它可以透過Subtype'Base 表示法命名。這個Name'Attribute(讀作“name tick attribute”)是 Ada 中用於所謂的屬性的特殊表示法,即由編譯器定義的型別、變數或其他程式實體的特徵,可以查詢。在本例中,基本型別(Subtype'Base)包含第一個子型別的所有值。一些例子
typeIntisrange0 .. 100;
基本型別Int'Base 是由編譯器選擇的硬體型別,它包含Int的值。因此,它的範圍可能是 -27 .. 27-1 或 -215 .. 215-1 或任何其他此類型別。
typeEnumis(A, B, C, D);typeShortisnewEnumrangeA .. C;
Enum'Base 與Enum相同,但Short'Base 還包含文字D。
受限子型別
[edit | edit source]不定子型別的子型別,添加了約束。以下示例定義了一個 10 個字元的字串子型別。
subtypeString_10isString (1 .. 10);
您不能部分約束不受約束的子型別
typeMy_Arrayisarray(Integerrange<>, Integerrange<>)ofSome_Type; --subtypeConstrisMy_Array (1 .. 10, Integerrange<>); illegalsubtypeConstrisMy_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。
不定子型別的子型別,如果它不新增約束,只會為原始子型別引入一個新名稱(一種在不同概念下的重新命名)。
subtypeMy_StringisString;
My_String和字串是可互換的。
命名子型別
[edit | edit source]分配了名稱的子型別。“第一個子型別”是使用關鍵字 建立的(請記住,型別總是匿名的,型別宣告中的名稱是第一個子型別的名稱),其他子型別是使用關鍵字type 建立的。例如subtype
typeCount_To_Tenisrange1 .. 10;
Count_to_Ten 是適合的整數基本型別的第一個子型別。但是,如果您想將其用作String 的索引約束,則以下宣告是非法的
subtypeTen_CharactersisString (Count_to_Ten);
這是因為String 的索引是Positive,它是Integer 的子型別(這些宣告取自包Standard)
subtypePositiveisIntegerrange1 .. Integer'Last;typeStringis(Positiverange<>)ofCharacter;
因此,您必須使用以下宣告
subtypeCount_To_TenisIntegerrange1 .. 10;subtypeTen_CharactersisString (Count_to_Ten);
現在,Ten_Characters 是String 的那個子型別的名稱,它被約束為Count_To_Ten。您會發現對型別和子型別施加約束具有非常不同的效果。
不受約束的子型別
[edit | edit source]任何不定型別也是不受約束的子型別。但是,不受約束和不定性並不相同。
typeMy_Enumis(A, B, C);typeMy_Record (Discriminant: My_Enum)is...; My_Object_A: My_Record (A);
此型別不受約束且不定,因為您需要提供物件宣告的實際辨別符;該物件被約束為此辨別符,而此辨別符不能更改。
但是,當為辨別符提供預設值時,該型別是確定性的,但不受約束;它允許定義約束和不受約束的物件
typeMy_Enumis(A, B, C);typeMy_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]typeMy_Integerisrange-10 .. + 10;subtypeMy_PositiveisMy_Integerrange+ 1 .. + 10;subtypeMy_NegativeisMy_Integerrange-10 .. - 1;
這些子型別當然是不相容的。
另一個例子是辨別記錄的子型別
typeMy_Enumis(A, B, C);typeMy_Record (Discriminant: My_Enum)is...;subtypeMy_A_RecordisMy_Record (A);subtypeMy_C_RecordisMy_Record (C);
這些子型別也是不相容的。
限定表示式
[edit | edit source]在大多數情況下,編譯器能夠推斷表示式的型別;例如
typeEnumis(A, B, C); E : Enum := A;
這裡,編譯器知道A 是型別Enum 的值。但考慮
procedureBadistypeEnum_1is(A, B, C);procedureP (E :inEnum_1)is... -- omittedtypeEnum_2is(A, X, Y, Z);procedureP (E :inEnum_2)is... -- omittedbeginP (A); -- illegal: ambiguousendBad;
編譯器無法在兩個版本的P 之間進行選擇;兩者都是同樣有效的。為了消除歧義,您使用限定表示式
P (Enum_1'(A)); -- OK
如以下示例所示,這種語法在建立新物件時經常使用。如果您嘗試編譯此示例,它將失敗並出現編譯錯誤,因為編譯器將確定 256 不在Byte 的範圍內。
withAda.Text_IO;procedureConvert_Evaluate_AsistypeByteismod2**8;typeByte_PtrisaccessByte;packageT_IOrenamesAda.Text_IO;packageM_IOisnewAda.Text_IO.Modular_IO (Byte); A :constantByte_Ptr :=newByte'(256);beginT_IO.Put ("A = "); M_IO.Put (Item => A.all, Width => 5, Base => 10);endConvert_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).
此示例說明了顯式型別轉換
withAda.Text_IO;procedureConvert_CheckedistypeShortisrange-128 .. +127;typeByteismod256;packageT_IOrenamesAda.Text_IO;packageI_IOisnewAda.Text_IO.Integer_IO (Short);packageM_IOisnewAda.Text_IO.Modular_IO (Byte); A : Short := -1; B : Byte;beginB := 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);endConvert_Checked;
在任何兩種數值型別之間都可能進行顯式轉換:整數、定點型別和浮點型別。如果涉及的型別之一是定點型別或浮點型別,編譯器不僅會檢查範圍約束(因此上面的程式碼將引發 Constraint_Error),還會執行任何必要的精度損失。
示例 1:精度損失會導致過程始終只打印“0”或“1”,因為P / 100是整數,始終為零或一。
withAda.Text_IO;procedureNaive_Explicit_ConversionistypeProportionisdigits4range0.0 .. 1.0;typePercentageisrange0 .. 100;functionTo_Proportion (P :inPercentage)returnProportionisbeginreturnProportion (P / 100);endTo_Proportion;beginAda.Text_IO.Put_Line (Proportion'Image (To_Proportion (27)));endNaive_Explicit_Conversion;
示例 2:我們使用中間浮點型別來保證精度。
withAda.Text_IO;procedureExplicit_ConversionistypeProportionisdigits4range0.0 .. 1.0;typePercentageisrange0 .. 100;functionTo_Proportion (P :inPercentage)returnProportionistypePropisdigits4range0.0 .. 100.0;beginreturnProportion (Prop (P) / 100.0);endTo_Proportion;beginAda.Text_IO.Put_Line (Proportion'Image (To_Proportion (27)));endExplicit_Conversion;
您可能想知道為什麼您應該在同一型別的兩個子型別之間進行轉換。一個例子將說明這一點。
subtypeString_10isString (1 .. 10); X: String := "A line long enough to make the example valid"; Slice:constantString := String_10 (X (11 .. 20));
在這裡,Slice 的邊界為 1 和 10,而 X (11 .. 20) 的邊界為 11 和 20。
表示形式改變
[edit | edit source]型別轉換可用於記錄或陣列的打包和解包。
typeUnpackedisrecord-- any componentsendrecord;typePackedisnewUnpacked;forPackeduserecord-- component clauses for some or for all componentsendrecord;
P: Packed; U: Unpacked; P := Packed (U); -- packs U U := Unpacked (P); -- unpacks P
非數值型別的檢查轉換
[edit | edit source]上面的示例都圍繞著數值型別之間的轉換;可以透過這種方式在任何兩種數值型別之間進行轉換。但是非數值型別之間會發生什麼,例如陣列型別或記錄型別之間?答案是雙重的
- 您可以顯式地在型別與其派生型別之間進行轉換,或者在從同一型別派生的型別之間進行轉換。
- 僅此而已。沒有其他轉換是可能的。
您為什麼要從另一個記錄型別派生記錄型別?由於表示子句。在這裡,我們進入了低階系統程式設計領域,這並不適合膽小的人,也不適用於桌面應用程式。所以堅持住,讓我們深入研究一下。
假設您有一個使用預設有效表示的記錄型別。現在您要將此記錄寫入使用特殊記錄格式的裝置。此特殊表示更緊湊(使用更少的位),但效率極低。您希望有一個分層的程式設計介面:面向應用程式的上層使用有效表示。下層是直接訪問硬體並使用無效表示的裝置驅動程式。
packageDevice_DriveristypeSize_Typeisrange0 .. 64;typeRegisterisrecordA, B : Boolean; Size : Size_Type;endrecord;procedureRead (R :outRegister);procedureWrite (R :inRegister);endDevice_Driver;
編譯器為Register 選擇了一個預設的有效表示。例如,在 32 位機器上,它可能會使用三個 32 位字,一個用於 A,一個用於 B,一個用於 Size。這種有效的表示對於應用程式來說很好,但在某一點上,我們希望將整個記錄轉換為僅 8 位,因為這是我們的硬體所需的。
packagebodyDevice_DriveristypeHardware_RegisterisnewRegister; -- Derived type.forHardware_RegisteruserecordAat0range0 .. 0; Bat0range1 .. 1; Sizeat0range2 .. 7;endrecord;functionGetreturnHardware_Register; -- Body omittedprocedurePut (H :inHardware_Register); -- Body omittedprocedureRead (R :outRegister)isH : Hardware_Register := Get;beginR := Register (H); -- Explicit conversion.endRead;procedureWrite (R :inRegister)isbeginPut (Hardware_Register (R)); -- Explicit conversion.endWrite;endDevice_Driver;
在上面的示例中,包體聲明瞭一個帶有無效但緊湊表示的派生型別,並將其轉換為它。
這說明了型別轉換會導致表示形式發生改變。
面向物件程式設計中的檢視轉換
[edit | edit source]在面向物件程式設計中,您必須區分特定型別和類範圍型別。
對於特定型別,只允許從根方向進行轉換,這當然不會失敗。不會有相反方向的轉換(你從哪裡得到額外的元件?);必須使用擴充套件聚合。
在轉換本身中,源物件中不存在於目標物件中的任何元件都不會丟失,它們只是隱藏在可見性之外。因此,這種型別的轉換稱為檢視轉換,因為它提供了將源物件作為目標型別的物件的檢視(特別是它不會更改物件的標籤)。
在面向物件程式設計中,對檢視轉換的結果進行重新命名是一種常見習慣。(重新命名宣告不會建立新的物件;它只是為已經存在的東西提供一個新名稱。)
typeParent_Typeistaggedrecord<components>;endrecord;typeChild_TypeisnewParent_Typewithrecord<further components>;endrecord; Child_Instance : Child_Type; Parent_View : Parent_TyperenamesParent_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_Instance 和Success 相等。第二次轉換未能透過標籤檢查。 (這種轉換賦值很少會使用;排程會自動完成,參見 面向物件程式設計。)
您可以使用成員資格測試自己執行這些檢查
ifParent_ViewinChild_Typethen...ifParent_ViewinChild_Type'Classthen...
還有包Ada.Tags。
地址轉換
[edit | edit source]Ada 的 訪問型別 不僅僅是一個記憶體位置(一個薄指標)。根據實現和所使用的 訪問型別,訪問 可能會保留額外的資訊(一個胖指標)。例如,GNAT 為每個對不確定物件的 訪問 保留兩個記憶體地址——一個用於資料,一個用於約束資訊('Size, 'First, 'Last).
如果您想將訪問轉換為簡單記憶體位置,可以使用包System.Address_To_Access_Conversions。但是請注意,地址和胖指標不能相互逆轉。
陣列物件的地址是其第一個元件的地址。因此,在這樣的轉換中會丟失邊界。
typeMy_Arrayisarray(Positiverange<>)ofSomething; A: My_Array (50 .. 100); A'Address = A(A'First)'Address
未檢查的轉換
[edit | edit source]對 Pascal 的一大批評是“沒有逃生之路”。原因是,有時您必須轉換不相容的東西。為此,Ada 有一個泛型函式Unchecked_Conversion
generictypeSource (<>)islimitedprivate;typeTarget (<>)islimitedprivate;functionAda.Unchecked_Conversion (S : Source)returnTarget;
Unchecked_Conversion 將按位複製源資料,並在目標型別下重新解釋它們,而無需任何檢查。您需要確保滿足 RM 13.9 (Annotated) 中規定的未檢查轉換要求;如果不滿足,結果將與實現相關,甚至可能導致異常資料。在有問題的案例中,使用轉換後的 'Valid 屬性來檢查資料的有效性。
對(Unchecked_Conversion 的例項)的函式呼叫將複製源資料到目標。編譯器也可以就地進行轉換(每個例項都有約定Intrinsic)。
要使用Unchecked_Conversion,您需要例項化泛型。
在下面的示例中,您可以看到它是如何完成的。執行時,示例將輸出A = -1, B = 255。不會報告錯誤,但這是您預期的結果嗎?
withAda.Text_IO;withAda.Unchecked_Conversion;procedureConvert_UncheckedistypeShortisrange-128 .. +127;typeByteismod256;packageT_IOrenamesAda.Text_IO;packageI_IOisnewAda.Text_IO.Integer_IO (Short);packageM_IOisnewAda.Text_IO.Modular_IO (Byte);functionConvertisnewAda.Unchecked_Conversion (Source => Short, Target => Byte); A :constantShort := -1; B : Byte;beginB := 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);endConvert_Unchecked;
當然,在賦值語句 B := Convert (A); 中有一個範圍檢查。因此,如果 B 被定義為 B: Byte ,則會引發 range 0 .. 10;Constraint_Error 錯誤。
如果 Unchecked_Conversion 結果的複製在效能方面浪費太多,那麼你可以嘗試覆蓋,即地址對映。透過使用覆蓋,兩個物件共享相同的記憶體位置。如果你為其中一個賦值,另一個也會隨之改變。語法如下:
forTarget'Addressuseexpression;pragmaImport (Ada, Target);
其中 expression 定義源物件的地址。
雖然覆蓋看起來比 Unchecked_Conversion 更優雅,但你應該知道它們更危險,並且更有可能做錯事。例如,如果 Source'Size < Target'Size 並且你為 Target 賦值,你可能會無意中寫入分配給其他物件的記憶體。
你還要注意目標型別物件的隱式初始化,因為它們會覆蓋源物件的實際值。可以使用帶 Ada 約定的 Import 預編譯指令來防止這種情況,因為它避免了隱式初始化,RM B.1 (帶註釋的)。
下面的示例與“Unchecked Conversion”中的示例效果相同。
withAda.Text_IO;procedureConvert_Address_MappingistypeShortisrange-128 .. +127;typeByteismod256;packageT_IOrenamesAda.Text_IO;packageI_IOisnewAda.Text_IO.Integer_IO (Short);packageM_IOisnewAda.Text_IO.Modular_IO (Byte); A :aliasedShort; B :aliasedByte;forB'AddressuseA'Address;pragmaImport (Ada, B);beginA := -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);endConvert_Address_Mapping;
僅僅為了記錄:還有一種方法使用 Export 和 Import 預編譯指令。但是,由於這種方法比覆蓋更徹底地破壞了 Ada 的可見性和型別概念,因此它不適合在這個語言介紹中,留給專家處理。
如前所述,型別宣告
typeTisrange1 .. 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 型別,適當地應用與上述類似的規則。
型別可以由其他型別構成。例如,陣列型別由兩個型別構成,一個是陣列的索引型別,另一個是陣列的元素型別。然後,陣列表示一個關聯,即索引型別的一個值與元素型別的一個值之間的關聯。
typeColoris(Red, Green, Blue);typeIntensityisrange0 .. 255;typeColored_Pointisarray(Color)ofIntensity;
型別Color是索引型別,型別Intensity是陣列型別的元素型別Colored_Point. 請參見 陣列。
