跳转到内容

Ada 编程/类型系统

来自 Wikibooks,开放世界中的开放书籍

Ada. Time-tested, safe and secure.
Ada. 经久耐用,安全可靠。

Ada 的类型系统允许程序员构建强大的抽象,这些抽象代表现实世界,并为编译器提供有价值的信息,以便编译器能够在逻辑或设计错误成为 bug 之前找到它们。它是语言的核心,优秀的 Ada 程序员学会了利用它来获得巨大的优势。四个原则支配着类型系统

  • 类型:对数据进行分类的方法。字符是 'a' 到 'z' 之间的类型。整数是包括 0、1、2... 的类型。
  • 强类型:类型彼此不兼容,因此不能混合苹果和橘子。编译器不会猜测你的苹果是橘子。你必须明确地说 my_fruit = fruit(my_apple)。强类型减少了错误数量。这是因为开发人员可以非常轻松地将浮点数写入整数变量而不自知。现在你程序成功运行所需的数据在编译器切换类型时在转换过程中丢失了。Ada 会生气并拒绝开发人员的愚蠢错误,拒绝执行转换,除非明确告知。
  • 静态类型:在编译时进行类型检查,这使得能够更早地发现类型错误。
  • 抽象:类型表示现实世界或所面临的问题;而不是计算机如何以内部方式表示数据。有一些方法可以指定类型必须如何在位级别表示,但我们将把讨论推迟到另一章。抽象的一个例子是你的汽车。你实际上不知道它是如何工作的,你只知道这块笨重的金属块在移动。你使用的几乎所有技术都是为了简化构成它的复杂电路而抽象化的层 - 软件也是如此。你想要抽象,因为类中的代码比调试时没有解释的一百个 if 语句更有意义
  • 名称等价:与大多数其他语言中使用的结构等价相反。如果且仅当两个类型具有相同的名称时,它们才兼容;不是因为它们碰巧具有相同的大小或位表示。因此,你可以声明两个具有相同范围但完全不兼容的整数类型,或者声明两个具有完全相同组件但彼此不兼容的记录类型。

类型彼此不兼容。但是,每个类型可以有任意数量的子类型,这些子类型与其基本类型兼容,并且可能彼此兼容。有关子类型之间不兼容的示例,请参见下文。

预定义类型

[编辑 | 编辑源代码]

有几个预定义类型,但大多数程序员更喜欢定义自己的特定于应用程序的类型。但是,这些预定义类型作为独立开发的库之间的接口非常有用。显然,预定义库也使用这些类型。

这些类型在 标准 包中预定义

整数
此类型至少涵盖范围 .. (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,具有特殊语义。

系统系统.存储元素 预定义了一些主要用于低级编程和与硬件接口的类型。

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 的类型可以按如下方式分类。

特定类型与类范围类型

type T is ...  --  a specific type
  T'Class      --  the corresponding class-wide type (exists only for tagged types)

T'ClassT'Class'Class 相同。

具有特定类型参数的原始操作是非分派的,而具有类范围类型参数的原始操作是分派的。

可以通过派生特定类型来声明新类型; 原始操作通过派生继承。 您不能从类范围类型派生。

约束类型与非约束类型

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 分配最大大小,因此大小变化(可能会随着区分符变化而出现)不会出现问题。 另一种可能性是在堆上进行隐式动态分配,并在大小变化时重新分配,然后进行释放。

确定类型与不确定类型

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

非约束类型与不确定类型

请注意,非约束子类型不一定是不确定的,如上面 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 的范围内。

文件: 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}}

类型转换

[编辑 | 编辑源代码]

数据并不总是以您需要的格式出现。因此,您必须面对转换它们的任务。作为一门真正通用的语言,尤其侧重于“任务关键型”、“系统编程”和“安全”,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).

此示例说明了显式类型转换

文件: 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。

表示更改

[编辑 | 编辑源代码]

类型转换可用于记录或数组的打包和解包。

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

非数值类型的检查转换

[编辑 | 编辑源代码]

上面的示例都围绕数值类型之间的转换;可以通过这种方式在任何两种数值类型之间进行转换。但是,在非数值类型之间(例如,在数组类型或记录类型之间)会发生什么?答案是双重的

  • 您可以在类型与其派生类型之间进行显式转换,或者在从同一类型派生的类型之间进行转换,
  • 仅此而已。没有其他转换是可能的。

为什么要从另一个记录类型派生一个记录类型?因为表示子句。在这里,我们进入了低级系统编程的领域,这对于胆小者来说并不适合,也不适合桌面应用程序。因此,请坚持住,让我们深入研究。

假设您有一个记录类型,它使用默认的有效表示。现在,您想将此记录写入设备,该设备使用特殊的记录格式。这种特殊表示更紧凑(使用更少的位),但效率极低。您想要有一个分层编程接口:面向应用程序的上层使用有效表示。下层是一个设备驱动程序,它直接访问硬件并使用低效表示。

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;

在上面的示例中,包主体声明了一个具有低效但紧凑表示的派生类型,并转换到该类型和从该类型转换。

这说明了类型转换会导致表示更改

面向对象编程中的视图转换

[编辑 | 编辑源代码]

面向对象编程中,您必须区分特定类型和类宽类型。

对于特定类型,只有朝根方向的转换是可能的,当然不会失败。没有反方向的转换(从哪里获取进一步的组件?)扩展聚合必须使用。

对于转换本身,源对象中不存在于目标对象中的任何组件都不会丢失,它们只是隐藏了。因此,这种转换被称为视图转换,因为它提供了一个作为目标类型对象的源对象的视图(尤其它不会更改对象的标记)。

在面向对象编程中,为视图转换的结果重命名是一种常见的习惯用法。(重命名声明不会创建新的对象;它只是为已经存在的东西提供一个新名称。)

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

地址转换

[编辑 | 编辑源代码]

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 编译指示。但是,由于这种方法比覆盖更彻底地破坏了 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

在运行时,当然在相应的子类型上执行计算,包括范围检查和溢出检查。但是,中间结果可能会超出范围限制。因此,对于上面的子类型 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 Color is (Red, Green, Blue);
 type Intensity is range 0 .. 255;
 
 type Colored_Point is array (Color) of Intensity;

类型Color是索引类型,类型Intensity是数组类型 Colored_Point 的组件类型。Colored_Point. 见 array

另见

[edit | edit source]

维基教科书

[edit | edit source]

Ada 参考手册

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