跳转到内容

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 参考手册

[编辑 | 编辑源代码]
华夏公益教科书