跳转到内容

Ada 编程/类型系统

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

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

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

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

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

预定义类型

[编辑 | 编辑源代码]

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

这些类型在 Standard 包中预定义

整数
此类型至少涵盖范围 .. (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

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 语言的读者可以参考的 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 …;

(省略号表示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 的范围内。

File: convert_evaluate_as.adb (view, plain text, download page, browse all)
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]

显式类型转换看起来很像函数调用;它不像限定表达式那样使用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).

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

文件:convert_checked.adb (view, plain text, download page, browse all)
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 (查看, 纯文本, 下载页面, 浏览所有)
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 错误。

如果 Unchecked_Conversion 结果的复制在性能方面浪费太多,那么你可以尝试覆盖,即地址映射。通过使用覆盖,两个对象共享相同的内存位置。如果你为其中一个赋值,另一个也会随之改变。语法如下:

for Target'Address use expression;
pragma Import (Ada, Target);

其中 expression 定义源对象的地址。

虽然覆盖看起来比 Unchecked_Conversion 更优雅,但你应该知道它们更危险,并且更有可能做错事。例如,如果 Source'Size < Target'Size 并且你为 Target 赋值,你可能会无意中写入分配给其他对象的内存。

你还要注意目标类型对象的隐式初始化,因为它们会覆盖源对象的实际值。可以使用带 Ada 约定的 Import 预编译指令来防止这种情况,因为它避免了隐式初始化,RM B.1 (带注释的)

下面的示例与“Unchecked Conversion”中的示例效果相同。

文件: convert_address_mapping.adb (查看, 纯文本, 下载页面, 浏览所有)
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;

导出 / 导入

[编辑 | 编辑源代码]

仅仅为了记录:还有一种方法使用 ExportImport 预编译指令。但是,由于这种方法比覆盖更彻底地破坏了 Ada 的可见性和类型概念,因此它不适合在这个语言介绍中,留给专家处理。

关于有符号整型类型的详细讨论

[编辑 | 编辑源代码]

如前所述,类型声明

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 类型,适当地应用与上述类似的规则。

类型之间的关系

[编辑 | 编辑源代码]

类型可以由其他类型构成。例如,数组类型由两个类型构成,一个是数组的索引类型,另一个是数组的元素类型。然后,数组表示一个关联,即索引类型的一个值与元素类型的一个值之间的关联。

 type Color is (Red, Green, Blue);
 type Intensity is range 0 .. 255;
 
 type Colored_Point is array (Color) of Intensity;

类型Color是索引类型,类型Intensity是数组类型的元素类型Colored_Point. 请参见 数组

另请参见

[编辑 | 编辑源代码]

维基教科书

[编辑 | 编辑源代码]

Ada 参考手册

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