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. 类范围类型
type
Tis
... -- a specific type T'Class -- the corresponding class-wide type (exists only for tagged types)
具有特定类型参数的原始操作是非调度的,而具有类范围类型参数的原始操作是调度的。
可以通过从特定类型派生来声明新类型;原始操作通过派生来继承。 不能从类范围类型派生。
约束类型 vs. 无约束类型
type
Iis
range
1 .. 10; -- constrainedtype
ACis
array
(1 .. 10)of
... -- constrained
type
AUis
array
(Irange
<>)of
... -- unconstrainedtype
R (X: Discriminant [:= Default])is
... -- unconstrained
通过对无约束子类型进行约束,子类型或对象会变为约束类型。
subtype
RCis
R (Value); -- constrained subtype of R OC: R (Value); -- constrained object of anonymous constrained subtype of R OU: R; -- unconstrained object
仅当在上面的类型声明中给出默认值时,才有可能声明无约束对象。 语言没有指定这些对象是如何分配的。 GNAT 会分配最大大小,以便大小的更改(可能在判别式更改时出现)不会出现问题。 另一种可能性是在堆上进行隐式动态分配,并在大小更改时进行重新分配,然后进行释放。
确定性类型 vs. 不确定性类型
type
Iis
range
1 .. 10; -- definitetype
RD (X: Discriminant := Default)is
... -- definite
type
T (<>)is
... -- indefinitetype
AUis
array
(Irange
<>)of
... -- indefinitetype
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
Tis
limited
…;
(省略号表示private
,或者表示record
定义,请参阅本页面上的相应小节。)
您可以在 受限类型 章节中了解更多信息。
定义新类型和子类型
[edit | edit source]您可以使用以下语法定义新类型:
type
Tis
...
然后按照每种类型类别中的详细说明来描述类型。
从形式上讲,上面的声明创建了一个类型及其第一个子类型,名为 T
。 该类型本身,正确地称为“T 的类型”,是匿名的;RM 将其称为 T
(用斜体表示),但通常会不严谨地谈论类型 T。 但这是一个学术上的考虑;对于大多数目的,将 T
视为一种类型就足够了。 对于标量类型,还有一个称为 T'Base
的基本类型,它包含 T 的所有值。
对于有符号整数类型,T 的类型包含(完整)一组数学整数。 基本类型是一种特定的硬件类型,关于零对称(可能除一个额外的负值外),包含 T 的所有值。
如上所述,所有类型都是不兼容的;因此
type
Integer_1is
range
1 .. 10;type
Integer_2is
range
1 .. 10; A : Integer_1 := 8; B : Integer_2 := A; -- illegal!
是非法的,因为 Integer_1
和 Integer_2
是不同的且不兼容的类型。 正是由于这个特性,编译器才能在编译时检测到逻辑错误,例如将文件描述符添加到字节数或将长度添加到重量。 这两种类型具有相同范围这一事实并不能使它们兼容:这是名称等效性在起作用,而不是结构等效性。(下面,我们将看到如何对不兼容类型进行转换;这方面有严格的规则。)
创建子类型
[edit | edit source]您还可以创建给定类型的子类型,这些子类型彼此兼容,如下所示:
type
Integer_1is
range
1 .. 10;subtype
Integer_2is
Integer_1range
7 .. 11; -- badsubtype
Integer_3is
Integer_1'Baserange
7 .. 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
。
有一些非常有用的预定义子类型:
subtype
Naturalis
Integerrange
0 .. Integer'Last;subtype
Positiveis
Integerrange
1 .. Integer'Last;
派生类型
[edit | edit source]派生类型是从现有类型创建的一种全新的完整类型。 与任何其他类型一样,它与其父类型不兼容;但是,它继承了为父类型定义的原始操作。
type
Integer_1is
range
1 .. 10;type
Integer_2is
new
Integer_1range
2 .. 8; A : Integer_1 := 8; B : Integer_2 := A; -- illegal!
这里,两种类型都是离散的;派生类型的范围必须包含在其父类型的范围内。 将此与子类型进行对比。 原因是派生类型继承了为其父类型定义的原始操作,而这些操作假设了父类型的范围。 以下是如何演示此功能:
procedure
Derived_Typesis
package
Pakis
type
Integer_1is
range
1 .. 10;procedure
P (I:in
Integer_1); -- primitive operation, assumes 1 .. 10type
Integer_2is
new
Integer_1range
8 .. 10; -- must not break P's assumption -- procedure P (I: in Integer_2); inherited P implicitly defined hereend
Pak;package
body
Pakis
-- omittedend
Pak;use
Pak; A: Integer_1 := 4; B: Integer_2 := 9;begin
P (B); -- OK, call the inherited operationend
Derived_Types;
当我们调用P (B)
时,参数B将被转换为Integer_1
;这种转换当然会通过,因为派生类型(这里为8 .. 10)的可接受值集合必须包含在父类型(1 .. 10)的值集合中。然后P被调用,并带有转换后的参数。
但是,考虑上面例子的一个变体
procedure
Derived_Typesis
package
Pakis
type
Integer_1is
range
1 .. 10;procedure
P (I:in
Integer_1; J:out
Integer_1);type
Integer_2is
new
Integer_1range
8 .. 10;end
Pak;package
body
Pakis
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_Typesis
package
Pakis
type
Integer_1is
range
1 .. 10;procedure
P (I:in
Integer_1; J:out
Integer_1);type
Integer_2is
new
Integer_1'Baserange
8 .. 12;end
Pak;package
body
Pakis
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_Typeis
String (1 .. 10); X : Anonymous_String_Type := (others
=> ' ');
基本类型
[edit | edit source]在 Ada 中,所有类型都是匿名 的,只有子类型可以命名。对于标量类型,匿名类型有一个特殊的子类型,称为基本类型,它可以通过Subtype'Base
表示法命名。这个Name'Attribute
(读作“name tick attribute”)是 Ada 中用于所谓的属性的特殊表示法,即由编译器定义的类型、变量或其他程序实体的特征,可以查询。在本例中,基本类型(Subtype'Base
)包含第一个子类型的所有值。一些例子
type
Intis
range
0 .. 100;
基本类型Int'Base
是由编译器选择的硬件类型,它包含Int
的值。因此,它的范围可能是 -27 .. 27-1 或 -215 .. 215-1 或任何其他此类类型。
type
Enumis
(A, B, C, D);type
Shortis
new
Enumrange
A .. C;
Enum'Base
与Enum
相同,但Short'Base
还包含文字D
。
受限子类型
[edit | edit source]不定子类型的子类型,添加了约束。以下示例定义了一个 10 个字符的字符串子类型。
subtype
String_10is
String (1 .. 10);
您不能部分约束不受约束的子类型
type
My_Arrayis
array
(Integerrange
<>, Integerrange
<>)of
Some_Type; --subtype
Constris
My_Array (1 .. 10, Integerrange
<>); illegalsubtype
Constris
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_Stringis
String;
My_String和字符串是可互换的。
命名子类型
[edit | edit source]分配了名称的子类型。“第一个子类型”是使用关键字
创建的(请记住,类型总是匿名的,类型声明中的名称是第一个子类型的名称),其他子类型是使用关键字type
创建的。例如subtype
type
Count_To_Tenis
range
1 .. 10;
Count_to_Ten
是适合的整数基本类型的第一个子类型。但是,如果您想将其用作String
的索引约束,则以下声明是非法的
subtype
Ten_Charactersis
String (Count_to_Ten);
这是因为String
的索引是Positive
,它是Integer
的子类型(这些声明取自包Standard
)
subtype
Positiveis
Integerrange
1 .. Integer'Last;type
Stringis
(Positiverange
<>)of
Character;
因此,您必须使用以下声明
subtype
Count_To_Tenis
Integerrange
1 .. 10;subtype
Ten_Charactersis
String (Count_to_Ten);
现在,Ten_Characters
是String
的那个子类型的名称,它被约束为Count_To_Ten
。您会发现对类型和子类型施加约束具有非常不同的效果。
不受约束的子类型
[edit | edit source]任何不定类型也是不受约束的子类型。但是,不受约束和不定性并不相同。
type
My_Enumis
(A, B, C);type
My_Record (Discriminant: My_Enum)is
...; My_Object_A: My_Record (A);
此类型不受约束且不定,因为您需要提供对象声明的实际辨别符;该对象被约束为此辨别符,而此辨别符不能更改。
但是,当为辨别符提供默认值时,该类型是确定性的,但不受约束;它允许定义约束和不受约束的对象
type
My_Enumis
(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_Integeris
range
-10 .. + 10;subtype
My_Positiveis
My_Integerrange
+ 1 .. + 10;subtype
My_Negativeis
My_Integerrange
-10 .. - 1;
这些子类型当然是不兼容的。
另一个例子是辨别记录的子类型
type
My_Enumis
(A, B, C);type
My_Record (Discriminant: My_Enum)is
...;subtype
My_A_Recordis
My_Record (A);subtype
My_C_Recordis
My_Record (C);
这些子类型也是不兼容的。
限定表达式
[edit | edit source]在大多数情况下,编译器能够推断表达式的类型;例如
type
Enumis
(A, B, C); E : Enum := A;
这里,编译器知道A
是类型Enum
的值。但考虑
procedure
Badis
type
Enum_1is
(A, B, C);procedure
P (E :in
Enum_1)is
... -- omittedtype
Enum_2is
(A, X, Y, Z);procedure
P (E :in
Enum_2)is
... -- omittedbegin
P (A); -- illegal: ambiguousend
Bad;
编译器无法在两个版本的P
之间进行选择;两者都是同样有效的。为了消除歧义,您使用限定表达式
P (Enum_1'(A)); -- OK
如以下示例所示,这种语法在创建新对象时经常使用。如果您尝试编译此示例,它将失败并出现编译错误,因为编译器将确定 256 不在Byte
的范围内。
with
Ada.Text_IO;procedure
Convert_Evaluate_Asis
type
Byteis
mod
2**8;type
Byte_Ptris
access
Byte;package
T_IOrenames
Ada.Text_IO;package
M_IOis
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).
此示例说明了显式类型转换
with
Ada.Text_IO;procedure
Convert_Checkedis
type
Shortis
range
-128 .. +127;type
Byteis
mod
256;package
T_IOrenames
Ada.Text_IO;package
I_IOis
new
Ada.Text_IO.Integer_IO (Short);package
M_IOis
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_Conversionis
type
Proportionis
digits
4range
0.0 .. 1.0;type
Percentageis
range
0 .. 100;function
To_Proportion (P :in
Percentage)return
Proportionis
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_Conversionis
type
Proportionis
digits
4range
0.0 .. 1.0;type
Percentageis
range
0 .. 100;function
To_Proportion (P :in
Percentage)return
Proportionis
type
Propis
digits
4range
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_10is
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
Unpackedis
record
-- any componentsend
record
;type
Packedis
new
Unpacked;for
Packeduse
record
-- component clauses for some or for all componentsend
record
;
P: Packed; U: Unpacked; P := Packed (U); -- packs U U := Unpacked (P); -- unpacks P
非数值类型的检查转换
[edit | edit source]上面的示例都围绕着数值类型之间的转换;可以通过这种方式在任何两种数值类型之间进行转换。但是非数值类型之间会发生什么,例如数组类型或记录类型之间?答案是双重的
- 您可以显式地在类型与其派生类型之间进行转换,或者在从同一类型派生的类型之间进行转换。
- 仅此而已。没有其他转换是可能的。
您为什么要从另一个记录类型派生记录类型?由于表示子句。在这里,我们进入了低级系统编程领域,这并不适合胆小的人,也不适用于桌面应用程序。所以坚持住,让我们深入研究一下。
假设您有一个使用默认有效表示的记录类型。现在您要将此记录写入使用特殊记录格式的设备。此特殊表示更紧凑(使用更少的位),但效率极低。您希望有一个分层的编程接口:面向应用程序的上层使用有效表示。下层是直接访问硬件并使用无效表示的设备驱动程序。
package
Device_Driveris
type
Size_Typeis
range
0 .. 64;type
Registeris
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_Driveris
type
Hardware_Registeris
new
Register; -- Derived type.for
Hardware_Registeruse
record
Aat
0range
0 .. 0; Bat
0range
1 .. 1; Sizeat
0range
2 .. 7;end
record
;function
Getreturn
Hardware_Register; -- Body omittedprocedure
Put (H :in
Hardware_Register); -- Body omittedprocedure
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_Typeis
tagged
record
<components>;end
record
;type
Child_Typeis
new
Parent_Typewith
record
<further components>;end
record
; Child_Instance : Child_Type; Parent_View : Parent_Typerenames
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_Instance
和Success
相等。第二次转换未能通过标签检查。 (这种转换赋值很少会使用;调度会自动完成,参见 面向对象编程。)
您可以使用成员资格测试自己执行这些检查
if
Parent_Viewin
Child_Typethen
...if
Parent_Viewin
Child_Type'Class
then
...
还有包Ada.Tags
。
地址转换
[edit | edit source]Ada 的 访问类型 不仅仅是一个内存位置(一个薄指针)。根据实现和所使用的 访问类型,访问 可能会保留额外的信息(一个胖指针)。例如,GNAT 为每个对不确定对象的 访问 保留两个内存地址——一个用于数据,一个用于约束信息('Size, 'First, 'Last).
如果您想将访问转换为简单内存位置,可以使用包System.Address_To_Access_Conversions
。但是请注意,地址和胖指针不能相互逆转。
数组对象的地址是其第一个组件的地址。因此,在这样的转换中会丢失边界。
type
My_Arrayis
array
(Positiverange
<>)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
。不会报告错误,但这是您预期的结果吗?
with
Ada.Text_IO;with
Ada.Unchecked_Conversion;procedure
Convert_Uncheckedis
type
Shortis
range
-128 .. +127;type
Byteis
mod
256;package
T_IOrenames
Ada.Text_IO;package
I_IOis
new
Ada.Text_IO.Integer_IO (Short);package
M_IOis
new
Ada.Text_IO.Modular_IO (Byte);function
Convertis
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'Addressuse
expression;pragma
Import (Ada, Target);
其中 expression 定义源对象的地址。
虽然覆盖看起来比 Unchecked_Conversion
更优雅,但你应该知道它们更危险,并且更有可能做错事。例如,如果 Source'Size < Target'Size
并且你为 Target 赋值,你可能会无意中写入分配给其他对象的内存。
你还要注意目标类型对象的隐式初始化,因为它们会覆盖源对象的实际值。可以使用带 Ada 约定的 Import 预编译指令来防止这种情况,因为它避免了隐式初始化,RM B.1 (带注释的)。
下面的示例与“Unchecked Conversion”中的示例效果相同。
with
Ada.Text_IO;procedure
Convert_Address_Mappingis
type
Shortis
range
-128 .. +127;type
Byteis
mod
256;package
T_IOrenames
Ada.Text_IO;package
I_IOis
new
Ada.Text_IO.Integer_IO (Short);package
M_IOis
new
Ada.Text_IO.Modular_IO (Byte); A :aliased
Short; B :aliased
Byte;for
B'Addressuse
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;
仅仅为了记录:还有一种方法使用 Export 和 Import 预编译指令。但是,由于这种方法比覆盖更彻底地破坏了 Ada 的可见性和类型概念,因此它不适合在这个语言介绍中,留给专家处理。
如前所述,类型声明
type
Tis
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
Coloris
(Red, Green, Blue);type
Intensityis
range
0 .. 255;type
Colored_Pointis
array
(Color)of
Intensity;
类型Color是索引类型,类型Intensity是数组类型的元素类型Colored_Point. 请参见 数组。