Ada 编程/类型系统
Ada 的类型系统允许程序员构建强大的抽象,这些抽象代表现实世界,并为编译器提供有价值的信息,以便编译器能够在逻辑或设计错误成为 bug 之前找到它们。它是语言的核心,优秀的 Ada 程序员学会了利用它来获得巨大的优势。四个原则支配着类型系统
- 类型:对数据进行分类的方法。字符是 'a' 到 'z' 之间的类型。整数是包括 0、1、2... 的类型。
- 强类型:类型彼此不兼容,因此不能混合苹果和橘子。编译器不会猜测你的苹果是橘子。你必须明确地说 my_fruit = fruit(my_apple)。强类型减少了错误数量。这是因为开发人员可以非常轻松地将浮点数写入整数变量而不自知。现在你程序成功运行所需的数据在编译器切换类型时在转换过程中丢失了。Ada 会生气并拒绝开发人员的愚蠢错误,拒绝执行转换,除非明确告知。
- 静态类型:在编译时进行类型检查,这使得能够更早地发现类型错误。
- 抽象:类型表示现实世界或所面临的问题;而不是计算机如何以内部方式表示数据。有一些方法可以指定类型必须如何在位级别表示,但我们将把讨论推迟到另一章。抽象的一个例子是你的汽车。你实际上不知道它是如何工作的,你只知道这块笨重的金属块在移动。你使用的几乎所有技术都是为了简化构成它的复杂电路而抽象化的层 - 软件也是如此。你想要抽象,因为类中的代码比调试时没有解释的一百个 if 语句更有意义
- 名称等价:与大多数其他语言中使用的结构等价相反。如果且仅当两个类型具有相同的名称时,它们才兼容;不是因为它们碰巧具有相同的大小或位表示。因此,你可以声明两个具有相同范围但完全不兼容的整数类型,或者声明两个具有完全相同组件但彼此不兼容的记录类型。
类型彼此不兼容。但是,每个类型可以有任意数量的子类型,这些子类型与其基本类型兼容,并且可能彼此兼容。有关子类型之间不兼容的示例,请参见下文。
有几个预定义类型,但大多数程序员更喜欢定义自己的特定于应用程序的类型。但是,这些预定义类型作为独立开发的库之间的接口非常有用。显然,预定义库也使用这些类型。
这些类型在 标准 包中预定义
- 整数
- 此类型至少涵盖范围 .. (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.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 中的等价物。
- 有符号整数 (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
Tis
... -- a specific type T'Class -- the corresponding class-wide type (exists only for tagged types)
具有特定类型参数的原始操作是非分派的,而具有类范围类型参数的原始操作是分派的。
可以通过派生特定类型来声明新类型; 原始操作通过派生继承。 您不能从类范围类型派生。
约束类型与非约束类型
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 分配最大大小,因此大小变化(可能会随着区分符变化而出现)不会出现问题。 另一种可能性是在堆上进行隐式动态分配,并在大小变化时重新分配,然后进行释放。
确定类型与不确定类型
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
非约束类型与不确定类型
请注意,非约束子类型不一定是不确定的,如上面 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}}
数据并不总是以您需要的格式出现。因此,您必须面对转换它们的任务。作为一门真正通用的语言,尤其侧重于“任务关键型”、“系统编程”和“安全”,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).
此示例说明了显式类型转换
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。
类型转换可用于记录或数组的打包和解包。
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
上面的示例都围绕数值类型之间的转换;可以通过这种方式在任何两种数值类型之间进行转换。但是,在非数值类型之间(例如,在数组类型或记录类型之间)会发生什么?答案是双重的
- 您可以在类型与其派生类型之间进行显式转换,或者在从同一类型派生的类型之间进行转换,
- 仅此而已。没有其他转换是可能的。
为什么要从另一个记录类型派生一个记录类型?因为表示子句。在这里,我们进入了低级系统编程的领域,这对于胆小者来说并不适合,也不适合桌面应用程序。因此,请坚持住,让我们深入研究。
假设您有一个记录类型,它使用默认的有效表示。现在,您想将此记录写入设备,该设备使用特殊的记录格式。这种特殊表示更紧凑(使用更少的位),但效率极低。您想要有一个分层编程接口:面向应用程序的上层使用有效表示。下层是一个设备驱动程序,它直接访问硬件并使用低效表示。
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;
在上面的示例中,包主体声明了一个具有低效但紧凑表示的派生类型,并转换到该类型和从该类型转换。
这说明了类型转换会导致表示更改。
在面向对象编程中,您必须区分特定类型和类宽类型。
对于特定类型,只有朝根方向的转换是可能的,当然不会失败。没有反方向的转换(从哪里获取进一步的组件?);扩展聚合必须使用。
对于转换本身,源对象中不存在于目标对象中的任何组件都不会丢失,它们只是隐藏了。因此,这种转换被称为视图转换,因为它提供了一个作为目标类型对象的源对象的视图(尤其它不会更改对象的标记)。
在面向对象编程中,为视图转换的结果重命名是一种常见的习惯用法。(重命名声明不会创建新的对象;它只是为已经存在的东西提供一个新名称。)
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
。
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
。
覆盖
[edit | edit source]如果复制 Unchecked_Conversion
的结果在性能方面过于浪费,那么您可以尝试覆盖,即地址映射。通过使用覆盖,两个对象共享同一个内存位置。如果您为其中一个赋值,另一个也会随之改变。语法是
for
Target'Addressuse
expression;pragma
Import (Ada, Target);
其中 expression 定义源对象的地址。
虽然覆盖看起来比 Unchecked_Conversion
更优雅,但您应该意识到它们更危险,并且更可能做错事。例如,如果 Source'Size < Target'Size
,并且您为 Target 赋值,您可能会无意中写入分配给另一个对象的内存。
您还必须注意目标类型的对象的隐式初始化,因为它们会覆盖源对象的实际值。Import pragma with convention Ada 可以用于防止这种情况,因为它会避免隐式初始化,RM B.1 (Annotated)。
下面的示例与“未经检查的转换”部分的示例相同。
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;
导出/导入
[edit | edit source]仅供记录:还有另一种方法使用Export 和 Import 编译指示。但是,由于这种方法比覆盖更彻底地破坏了 Ada 的可见性和类型概念,因此在语言介绍中没有它的一席之地,并留给专家处理。
有符号整数类型的类型详细讨论
[edit | edit source]如前所述,类型声明
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
类型,并且相应的规则也适用。
类型之间的关系
[edit | edit source]类型可以从其他类型创建。例如,数组类型由两种类型组成,一种用于数组的索引,另一种用于数组的组件。然后,数组表示一种关联,即索引类型的一个值和组件类型的一个值之间的关联。
type
Coloris
(Red, Green, Blue);type
Intensityis
range
0 .. 255;type
Colored_Pointis
array
(Color)of
Intensity;
类型Color是索引类型,类型Intensity是数组类型 Colored_Point
的组件类型。Colored_Point. 见 array。