跳转到内容

Ada 样式指南/可移植性

来自 Wikibooks,为开放世界提供开放书籍

并发 · 可重用性

关于可移植性的讨论通常集中在计算机系统之间的差异,但开发和运行时环境也可能发生变化。

可移植性(软件)
软件从一个计算机系统或环境转移到另一个计算机系统或环境的难易程度(IEEE 词典 1984)。

大多数可移植性问题并非纯粹的语言问题。可移植性涉及硬件(字节序、设备 I/O)和软件(实用程序库、操作系统、运行时库)。本章不会讨论这些具有挑战性的设计问题。

本章确实确定了从一个平台或编译器迁移到另一个平台时,Ada 特定的更常见可移植性问题。它还建议了隔离不可移植代码的方法。通过使用 Ada 的实现隐藏功能,可以显着降低移植成本。

事实上,许多语言可移植性问题通过 Ada 语言本身的严格定义得到了解决。在大多数编程语言中,不同的方言盛行,因为供应商出于各种原因扩展或稀释语言:符合编程环境或针对特定应用领域的特性。Ada 编译器验证能力 (ACVC) 由美国国防部在 Ada 验证机构 ASD/SIDL(莱特-帕特森空军基地)开发,以确保实现者严格遵守 Ada 标准。

作为 Ada 严格定义的一部分,某些结构被定义为错误,执行错误结构的效果是不可预测的。因此,错误结构显然不可移植。错误结构和有界错误在指南 5.9.10 中讨论,本章不再重复。

大多数刚接触这种语言的程序员希望 Ada 消除所有可移植性问题;它绝对没有。Ada 的某些领域尚未涵盖验证。Ada 的定义将某些细节留给实现者。编译器实现者在这些细节方面做出的选择会影响可移植性。

1995 年标准中批准的 Ada 语言修订版产生了新的可移植性问题领域。有些程序旨在具有较长的生命周期,可能从 Ada 83(Ada 参考手册 1983)开始,但过渡到 Ada 95(Ada 参考手册 1995)。虽然本样式指南侧重于当前的 Ada 标准,并没有解决过渡问题,但使用语言的某些特性会存在可移植性问题。这些问题围绕着在 Ada 参考手册(1995)的附录 J 中指定为过时的语言特性。

语言的结构是为了满足一系列需求而开发的。即使这些结构可能影响可移植性,也可以合法地使用它们。本章中的许多指南都体现了一些提高可移植性的通用原则。他们是

  • 识别可能对相关实现或平台上的可移植性产生不利影响的 Ada 结构。
  • 依赖那些依赖所有相关实现共享的特征的 Ada 结构。避免使用那些实现特征在相关平台上不同的结构。
  • 如果必须使用程序的不可移植特性,请对其进行本地化和封装。
  • 突出显示可能导致可移植性问题的结构的使用。

这些指南不能漫不经心地应用。其中许多涉及对 Ada 模型及其实现的详细了解。在许多情况下,您将不得不权衡效率和可移植性之间的利弊。阅读本章应该会提高您对所涉及的权衡的认识。本章中的内容主要来自三个来源:Ada 运行时环境工作组 (ARTEWG) 的 Ada 运行时实现依赖项目录 (ARTEWG 1986);Nissen 和 Wallis 关于 Ada 中的可移植性和样式的书 (Nissen 和 Wallis 1984);以及 SofTech 为美国空军撰写的一篇关于 Ada 可移植性指南的论文 (Pappas 1985)。最后一种来源 (Pappas 1985) 包含了其他两种来源,并对问题进行了深入解释,提供了大量示例和最小化可移植性问题的技术。Conti (1987) 是了解 Ada 实现者允许的自由度和经常用于做出决定的标准的宝贵参考。

本章的目的是以本书的指南格式提供可移植性问题的摘要。本章并未包含参考文献中标识的所有问题,而仅包含最重要的问题。有关深入介绍,请参见 Pappas (1985)。本章还介绍了一些额外的指南,并在适用的情况下对其他指南进行了详细说明。有关 Ada I/O 可移植性问题的进一步阅读,请参见 Matthews (1987)、Griest (1989) 和 CECOM (1989)。

本章中的一些指南交叉引用并对本书中的其他指南施加了更严格的限制。这些限制适用于强调可移植性时。

本章中的指南经常用“考虑……”措辞,因为硬性规定不能适用于所有情况。您在特定情况下做出的具体选择涉及设计权衡。这些指南的基本原理旨在让您洞悉其中的一些权衡。

基础知识

[编辑 | 编辑源代码]

本节介绍了一些编写可移植 Ada 程序的通用原则。它包括关于您应该对许多 Ada 特性及其实现做出的假设的指南,以及关于使用其他 Ada 特性以确保最大可移植性的指南。

过时特性

[编辑 | 编辑源代码]
  • 在旨在具有较长生命周期的程序或组件中,避免使用 Ada 参考手册(1995)的附录 J 声明为“过时”的 Ada 特性,除非需要使用该特性才能向后兼容 Ada 83(Ada 参考手册 1983)。
  • 记录任何过时特性的使用情况。
  • 避免使用以下特性
    • 预定义环境中包的简短重命名(例如,Text_IO 而不是 Ada.Text_IO)
    • 对 ! 替代为 |, : 替代为 #,以及 % 替代为引号的字符替换
    • 浮点类型的精度降低的子类型
    • 应用于私有类型的'Constrained 属性
    • 预定义的包 ASCII
    • 异常 Numeric_Error
    • 各种表示规范,包括 at 子句、mod 子句、中断入口和 Storage_Size 属性

基本原理

[编辑 | 编辑源代码]

十年对 Ada 83 使用情况的反思得出结论,原始语言中的一些特性不像最初预期的那样有用。这些特性在 Ada 95 修订版中被其他特性所取代。理想情况下,应该完全删除过时的特性,但这会阻止程序从 Ada 83 向 Ada 95 的向上兼容过渡。因此,过时的特性仍然存在于语言中,并在 Ada 参考手册(1995)的附录 J 中明确标注为过时。附录 J 中列出的特性是语言在下次修订时删除的候选特性。如果程序的寿命可能超出下次语言修订版,则应避免使用过时的语言特性,除非与 Ada 83 的向后兼容性强制使用它们。

当您实例化 Ada.Text_IO.Float_IO 时,Default_Fore 和 Default_Aft 字段的值将从用于实例化的实际浮点类型的'Fore 和'Aft 属性的值设置。如果您声明一个精度降低的浮点类型,然后使用它来实例化 Ada.Text_IO.Float_IO,则输出字段宽度将根据精度降低的类型确定,尽管实现精度保持不变(基本原理 1995,§3.3)。

全局假设

[编辑 | 编辑源代码]
  • 对以下内容在潜在目标平台上提供的支持做出明智的假设
    • 整数类型可用位数(范围约束)
    • 浮点类型可用的精度小数位数
    • 定点类型可用位数(增量和范围约束)
    • 源文本每行字符数
    • Root_Integer 表达式位数
    • Duration 范围内的秒数
    • Duration'Small 的毫秒数
    • 十进制类型的最小和最大比例
  • 避免对 Character 类型中包含的值和值的数量做出假设。

实例化

[编辑 | 编辑源代码]
  • 这些是项目或应用程序可能假设的实现提供的最小值(或 Duration'Small 的最小精度)。不能保证给定的实现提供超过最小值的数目,因此项目或应用程序也会将这些视为最大值。

基本原理

[编辑 | 编辑源代码]

必须对某些实现特定的值做出一些假设。假设的精确值应该涵盖大多数目标设备。选择值的最低公分母可以提高可移植性。实现可能会提供特定于区域设置或环境的备用字符集。例如,IBM 兼容 PC 上的实现可能支持该机器的本机字符集而不是 Latin 1。因此,某些字符值可能支持也可能不支持,例如,笑脸。

目前可用于嵌入式系统中的微型计算机中,16 位和 32 位处理器很常见。使用当前的表示方案,6 位小数的浮点精度意味着表示尾数至少 21 位宽,在 32 位表示中为指数和符号留下 11 位。这与目前可用于嵌入式系统市场的浮点硬件的数据宽度相对应。定点数字的 32 位最小值与浮点数字的精度和存储要求相对应。Root_Integer 表达式的 16 位示例与 Integer 存储相匹配。(如果应用程序仅针对具有相应 32 位操作系统和支持编译器的 32 位处理器,则可以假设 32 位整数。)

预定义类型 Duration 的值范围和精度的值是在 Ada 参考手册 1995,第 9.6 节 [注释] 中表达的限制。你不应该期望实现提供更宽的范围或更细的粒度。

在大多数情况下,可以假设标准模式 Ada 字符集 Latin 1 用于类型 Character 和包 Character.Latin_1、Character.Handling 和 Strings.Maps 的内容和内部行为。但是,这并不意味着目标硬件平台能够显示整个字符集。除非有意生成具有特定目的的不可移植用户界面,否则你不应该使用非标准 Ada 字符集。

  • 对每个包、子程序和任务使用突出显示的注释,其中存在任何不可移植的特性。
  • 对于每个使用的不可移植特性,描述对该特性的期望。
------------------------------------------------------------------------
package Memory_Mapped_IO is
   -- WARNING - This package is implementation specific.
   -- It uses absolute memory addresses to interface with the I/O
   -- system. It assumes a particular printer's line length.
   -- Change memory mapping and printer details when porting.
   Printer_Line_Length : constant := 132;
   type Data is array (1 .. Printer_Line_Length) of Character;
   procedure Write_Line (Line : in     Data);
end Memory_Mapped_IO;
------------------------------------------------------------------------
with System;
with System.Storage_Elements;
package body Memory_Mapped_IO is
   -- WARNING: Implementation specific memory address

   Buffer_Address : constant System.Address
      := System.Storage_Elements.To_Address(16#200#);

   ---------------------------------------------------------------------
   procedure Write_Line (Line : in     Data) is
      Buffer : Data;
      for Buffer'Address use Buffer_Address;

   begin  -- Write_Line
       -- perform output operation through specific memory locations.
       ...
   end Write_Line;
   ---------------------------------------------------------------------
end Memory_Mapped_IO;
------------------------------------------------------------------------

基本原理

[编辑 | 编辑源代码]

明确地注释每个可移植性违规将提高其可见性并帮助移植过程。对不可移植特性的期望的描述涵盖了原始实现的供应商文档不可用于执行移植过程的人员的常见情况。

主子程序

[编辑 | 编辑源代码]
  • 考虑仅使用无参数过程作为主子程序。
  • 考虑使用 Ada.Command_Line 访问环境中的值,但要认识到该包的行为甚至其规范是不可移植的(参见指南 7.1.6)。
  • 封装和记录所有对包 Ada.Command_Line 的使用。

以下示例封装了从环境传递的假设“执行模式”参数的参数。它封装了参数的预期位置和预期值,并在环境无法提供信息的情况下提供默认值

package Environment is

   type Execution_Mode is (Unspecified, Interactive, Batch);

   function Execution_Argument return Execution_Mode;

   ...

end Environment;

----------------------------------------------------------------------

with Ada.Command_Line;       use Ada.Command_Line;
with Ada.Strings.Unbounded;  use Ada.Strings.Unbounded;

package body Environment is

   function Execution_Argument return Execution_Mode is

      Execution_Argument_Number : constant := 1;

      Interactive_Mode_String : constant String := "-i";
      Batch_Mode_String       : constant String := "-b";

   begin
      if Argument_Count < Execution_Argument_Number then
         return Unspecified;
      elsif To_Unbounded_String (Argument (Execution_Argument_Number)) =
               Interactive_Mode_String then
         return Interactive;
      elsif To_Unbounded_String (Argument (Execution_Argument_Number)) =
               Batch_Mode_String then
         return Batch;
      else
         return Unspecified;
      end if;
   end Execution_Argument;

end Environment;

基本原理

[编辑 | 编辑源代码]

预定义的语言环境声明包 Ada.Command_Line,它提供了一种标准化的方式,使程序能够获取命令行的值。因为所有 Ada 编译器都必须实现预定义语言环境中的包,所以你可以使用该包创建一个更可移植、可维护和可读的程序。但是,你应该意识到,尽管语言定义了该包的对象和类型配置文件,但它并没有强制函数结果与任何其他实体或操作之间存在关系,因此,允许可能存在不可移植的行为和规范。

函数 Ada.Command_Line.Argument_Count 返回的值是实现相关的。不同的操作系统对命令行参数的解析和含义遵循不同的约定。为了提高程序的可移植性,假设最简单的情况:外部执行环境不支持向程序传递参数。

一些操作系统能够从函数中获取和解释接近 0 的返回整数值,但许多其他操作系统则不能。此外,许多实时嵌入式系统并非设计为终止,因此具有 out 或 in out 模式参数的函数或过程将不适用于此类应用程序。

这留下了具有 in 参数的过程。虽然一些操作系统能够在程序启动时将参数传递给程序,但其他操作系统则不能。此外,即使周围环境能够提供参数,实现也可能无法对这些参数执行类型检查。

实时嵌入式应用程序可能没有提供参数的“操作员”启动程序,在这种情况下,更适合让程序与包含适当常数值的包一起编译,或者让程序从开关设置或下载的辅助文件中读取必要的值。无论哪种情况,周围启动环境的变化都太大,无法依赖于由(子程序)参数向主子程序传递的最后时刻(程序)参数化所暗示的那种参数化。POSIX 5 提供了一个标准的操作系统命令行界面,该界面可能是 Ada 命令行工具的更合适的替代方案,具体取决于应用程序的实现系列。

封装实现依赖关系

[编辑 | 编辑源代码]
  • 创建专门设计的包,以隔离硬件和实现依赖,并且这些包的规范在移植时不会发生变化。
  • 如果硬件或实现相关的代码是出于机器或解决方案效率的原因,则要明确说明目标。
  • 对于隐藏实现依赖关系的包,为不同的目标环境维护不同的包体。
  • 将中断接收任务隔离到实现相关的包中。
  • 有关实现相关的功能列表,请参见 Ada 参考手册(1995 年)的附件 M。

例子

[edit | edit source]

参见指南 7.1.3。

基本原理

[edit | edit source]

将硬件和实现依赖项封装在包中,允许代码的其余部分忽略它们,从而实现完全可移植性。它还将依赖项本地化,从而清楚地表明哪些代码部分在移植程序时可能需要更改。

一些实现相关的功能可用于实现特定的性能或效率目标。对这些目标进行注释可确保程序员在移植到不同的实现时能够找到实现这些目标的合适方法,或明确地认识到它们无法实现。

中断入口是实现相关的功能,可能不受支持(例如,VAX Ada 使用编译指示将系统陷阱分配给“正常”的 rendezvous)。但是,在大多数嵌入式实时系统中无法避免中断入口,因此可以合理地假设 Ada 实现支持它们。中断的值由实现定义。将其隔离。

注释

[edit | edit source]

您可以使用 Ada 编写依赖于机器的程序,这些程序以与 Ada 模型一致的方式利用实现,但会在 Ada 允许实现自由的地方做出特定的选择。这些机器依赖项应与代码的任何其他实现相关功能以相同的方式进行处理。

实现添加的功能

[edit | edit source]

指南

[edit | edit source]
  • 避免使用供应商提供的包。
  • 避免使用添加到预定义包中的功能,这些功能未在 Ada 语言定义或专门需求附件中指定。

基本原理

[edit | edit source]

供应商添加的功能不太可能由其他实现提供。即使大多数供应商最终提供类似的附加功能,它们也不太可能具有相同的公式。实际上,不同的供应商可能使用相同的公式来表示(语义上)完全不同的功能。有关供应商提供的异常的更多信息,请参见指南 7.5.2。

Ada 引入了许多在 Ada 83(Ada 参考手册 1983 年)中不存在的新编译指示和属性。这些新的编译指示和属性可能会与实现定义的编译指示和属性冲突。

异常

[edit | edit source]

有许多类型的应用程序需要使用这些功能。例如,在供应商文件系统上进行标准化的多语言系统、与供应商产品紧密集成的应用程序(即用户界面)以及出于性能原因的嵌入式系统。将这些功能的使用隔离到包中。

如果供应商提供的包以可编译的源代码形式提供,则使用该包不会使程序不可移植,前提是该包不包含任何不可移植的代码并且可以合法地包含在您的程序中。

专门需求附件

[edit | edit source]

指南

[edit | edit source]
  • 使用专门需求附件中定义的功能,而不是供应商定义的功能。
  • 清楚地记录对来自专门需求附件的任何功能的使用(系统编程、实时系统、分布式系统、信息系统、数值和安全与安全性)。

基本原理

[edit | edit source]

专门需求附件定义了特定应用领域的标准,而不会扩展语言的语法。如果您依赖于附件中标准化的功能,而不是依赖于特定的供应商扩展,则可以更轻松地在供应商实现之间移植具有特定领域需求(例如,分布式系统、信息系统)的程序。附件的目的是为几个预计使用 Ada 的应用领域中面临的问题提供一致且统一的解决方法。由于不同的编译器会支持不同的附件集(如果有),因此如果您依赖于任何给定附件中定义的功能,您可能会遇到可移植性问题。

专门需求附件提供了超出核心语言定义的功能。由于编译器不需要支持专用附件,因此您应该尽可能地将这些功能的使用本地化。通过记录其用法,您将为未来的程序员留下潜在移植困难的记录。

对参数传递机制的依赖

[edit | edit source]

指南

[edit | edit source]
  • 不要编写其正确执行依赖于实现使用的特定参数传递机制的代码(Ada 参考手册 1995 年,§6.2 [Annotated];Cohen 1986)。
  • 如果子程序具有多个给定子类型的形式参数,其中至少一个为 [in] out,则确保子程序能够正确处理两个形式参数都表示同一实际对象的情况。

例子

[edit | edit source]

此程序的输出取决于所使用的特定参数传递机制

------------------------------------------------------------------------
with Ada.Integer_Text_IO;
procedure Outer is
   type Coordinates is
      record
         X : Integer := 0;
         Y : Integer := 0;
      end record;
   Outer_Point : Coordinates;
   ---------------------------------------------------------------------
   procedure Inner (Inner_Point : in out Coordinates) is
   begin
      Inner_Point.X := 5;
      -- The following line causes the output of the program to
      -- depend on the parameter passing mechanism.
      Ada.Integer_Text_IO.Put(Outer_Point.X);
   end Inner;
   ---------------------------------------------------------------------
begin  -- Outer
   Ada.Integer_Text_IO.Put(Outer_Point.X);
   Inner(Outer_Point);
   Ada.Integer_Text_IO.Put(Outer_Point.X);
end Outer;
------------------------------------------------------------------------

如果参数传递机制是通过复制,则标准输出文件上的结果为

0 0 5

如果参数传递机制是通过引用,则结果为

0 5 5

以下代码片段显示了在用表示同一对象的实际参数调用过程时存在潜在的有限错误的情况

procedure Test_Bounded_Error (Parm_1 : in out    Integer;
                              Parm_2 : in out Integer) is
   procedure Inner (Parm : in out Integer) is
   begin
      Parm := Parm * 10;
   end Inner;
begin
   Parm_2 := 5;
   Inner (Parm_1);
end Test_Bounded_Error;

在执行过程 Test_Bounded_Error 时,Parm_1 和 Parm_2 都表示对象 Actual_Parm。执行第一个语句后,对象 Actual_Parm 的值为 5。当调用过程 Inner 时,其形式参数 Parm 表示 Actual_Parm。无法确定它表示 Parm_1 的旧值(在本例中为 1)还是新值(在本例中为 5)。

Actual_Parm : Integer := 1;
...
Test_Bounded_Error (Actual_Parm, Actual_Parm);  -- potential bounded error

基本原理

[edit | edit source]

某些复合类型(未标记的记录和数组)可以通过复制或引用传递。如果有两个或多个相同类型的形式参数,其中一个或多个是可写的,那么您应该记录是否假定这些形式参数不表示相同的实际对象。同样,如果具有给定子类型的形式参数的子程序也向上级引用了此相同类型的对象,那么您应该记录是否假定形式参数表示与向上级引用中命名的对象不同的对象。在这些对象可以通过不同的形式参数路径访问的情况下,可能会引发 Program_Error 异常,可能会读取新值,或者可能会使用对象的旧值(Ada 参考手册 1995 年,§6.2 [Annotated])。

另请参见指南 8.2.7。

异常

[edit | edit source]

通常,在将 Ada 与外部代码进行接口时,无法避免对特定实现使用的参数传递机制的依赖。在这种情况下,将对外部代码的调用隔离在一个接口包中,该包导出不依赖于参数传递机制的操作。

任意顺序依赖项

[编辑 | 编辑源代码]
  • 避免依赖Ada中某些结构的评估顺序。

该程序的输出取决于子程序参数的评估顺序,但是Ada参考手册1995,第6.4节[注释]指定这些评估以任意顺序执行。

package Utilities is
   function Unique_ID return Integer;
end Utilities;

package body Utilities is

   ID : Integer := 0;

   function Unique_ID return Integer is
   begin
      ID := ID + 1;
      return ID;
   end Unique_ID;

end Utilities;

--------------------------------------------------------------------------------
with Ada.Text_IO;
with Utilities; use Utilities;
procedure P is
begin
   Ada.Text_IO.Put_Line (Integer'Image(Unique_ID) & Integer'Image(Unique_ID));
end P;

如果“&”函数的参数按文本顺序评估,则输出为

1 2

如果参数以相反顺序评估,则输出为

2 1

基本原理

[编辑 | 编辑源代码]

Ada语言将某些评估定义为以任意顺序发生(例如,子程序参数)。尽管依赖评估顺序可能不会对特定实现上的程序产生不利影响,但代码在移植时可能无法正确执行。例如,如果子程序调用的两个实际参数具有副作用,则程序的效果可能取决于评估顺序(Ada参考手册1995,第1.1.4节[注释])。避免任意顺序依赖,但也认识到,即使这种无意错误也可能禁止可移植性。

数值类型和表达式

[编辑 | 编辑源代码]

Ada与数值计算相关的功能的设计非常谨慎,以确保该语言可以在嵌入式系统和数学应用程序中使用,在这些应用程序中精度很重要。这些功能尽可能地实现了可移植性。但是,在最大程度地利用特定机器上数值计算的可用精度与最大程度地提高Ada数值结构的可移植性之间不可避免地存在权衡。这意味着,如果要保证生成的程序的完全可移植性,则必须非常谨慎地使用这些Ada功能,尤其是数值类型和表达式。

预定义的数值类型

[编辑 | 编辑源代码]
  • 避免在Standard包中使用预定义的数值类型。使用范围和位数声明,并让实现选择合适的表示形式。
  • 对于需要比全局假设提供的精度更高的程序,请定义一个包,该包声明私有类型和操作,具体需求见Pappas(1985),其中有完整解释和示例。
  • 考虑对以下内容使用预定义的数值类型(Integer,Natural,Positive):
    • 数组的索引,其中索引类型并不重要,例如String类型
    • “纯”数字,即没有关联物理单位的数字(例如,指数)
    • 用于控制重复或迭代计数的值

下面的第二个和第三个示例无法在字长为16位的机器上表示为Integer的子范围。下面的第一个示例允许编译器在必要时选择多字表示形式。

使用

type    Second_Of_Day is             range 0 .. 86_400;

而不是

type    Second_Of_Day is new Integer range 1 .. 86_400;

subtype Second_Of_Day is     Integer range 1 .. 86_400;

基本原理

[编辑 | 编辑源代码]

实现人员可以自由地定义预定义数值类型的范围。将代码从精度更高的实现移植到精度较低的实现是一个耗时且容易出错的过程。许多错误只有在运行时才会报告。

这不仅适用于数值计算。如果您忽略了为整数离散范围(数组大小,循环范围等)使用显式声明的类型,那么就会遇到一个很容易被忽略的问题(请参见指南5.5.1和5.5.2)。如果您在指定索引约束和其他离散范围时没有提供显式类型,则会假设预定义的整数类型。

当您明智地使用预定义的数值类型时,它们很有用。您不应使用它们来避免声明数值类型,否则您将失去强类型的优势。当您的应用程序处理不同类型的数量和单位时,您应该通过使用不同的数值类型来将它们分开。但是,如果您只是在迭代逼近算法中计算迭代次数,那么声明特殊的整数类型可能就过分了。预定义的求幂运算符**要求其右操作数的类型为整数。

您应该使用预定义的类型Natural和Positive来操作预定义语言环境中某些类型的值。String和Wide_String类型使用Positive类型的索引。如果您的代码使用不兼容的整数类型索引字符串,您将被迫进行类型转换,从而降低其可读性。如果您正在执行切片和串联等操作,那么您的数值数组索引的子类型可能无关紧要,您最好使用预定义的子类型。另一方面,如果您的数组表示一个表(例如,哈希表),那么您的索引子类型就很重要,您应该声明一个不同的索引类型。

该指南允许使用另一种方法。如指南7.1.5所建议,实现依赖关系可以封装在为此目的而设计的包中。这可能包括定义一个32位整数类型。然后,可以从该32位类型派生其他类型。

精度模型

[编辑 | 编辑源代码]
  • 当性能和精度是主要问题时,请使用支持数值附录(Ada参考手册1995,附录G)的实现。

基本原理

[编辑 | 编辑源代码]

数值附录定义了浮点和定点算术的精度和性能要求。该附录提供了一种“严格”模式,在该模式下,编译器必须支持这些要求。为了保证您的程序的数值性能可移植,您应该在严格模式下进行编译和链接。如果您的程序依赖于严格模式的数值属性,那么它只能移植到也支持严格数值模式的其他环境中。

浮点数的精度基于在存储器中可以精确表示的机器数。当寄存器包含的位数多于存储器时,寄存器中的计算结果可能介于两个机器数之间。可以使用'Pred和'Succ属性逐步遍历机器数。其他属性返回尾数、指数、基数和其他特征的值,这些特征属于浮点数和定点数。

精度分析

[编辑 | 编辑源代码]
  • 仔细分析您真正需要的精度。

基本原理

[编辑 | 编辑源代码]

浮点计算使用等效于实现的预定义浮点类型执行。内部计算中额外的“保护”位有时会降低Ada声明中必须指定的位数。这在程序要运行的实现之间可能不一致。它也可能导致错误地得出结论,即声明的类型足以满足所需的精度。

您应该选择数值类型声明以满足最低精度(最小位数),以提供所需的精度。需要仔细分析才能证明声明足够。当您迁移到精度较低的机器时,您可能可以使用相同的类型声明。

精度约束

[编辑 | 编辑源代码]
  • 不要逼近机器的精度极限。

基本原理

[edit | edit source]

仅仅因为两台不同的机器在浮点数尾数中使用相同数量的数字并不意味着它们将具有相同的算术属性。一些 Ada 实现可能比 Ada 要求的精度略好,因为它们有效地利用了机器。不要编写依赖于此的程序。

评论

[edit | edit source]

指南

[edit | edit source]
  • 注释程序的数值方面的分析和推导。

基本原理

[edit | edit source]

关于为什么程序中需要特定精度以及背后的决策和背景对于程序修改或移植非常重要。应该注释导致程序的底层数值分析。

子表达式求值

[edit | edit source]

指南

[edit | edit source]
  • 预测子表达式的值范围,以避免超出其基本类型的值范围。在数值类型上使用派生类型、子类型、因式分解和范围约束(参见指南 3.4.1、5.3.1 和 5.5.3)。

例子

[edit | edit source]

此示例改编自 Rationale (1995, §3.3)

with Ada.Text_IO;
with Ada.Integer_Text_IO;
procedure Demo_Overflow is
-- assume the predefined type Integer has a 16-bit range
   X : Integer := 24_000;
   Y : Integer;
begin  -- Demo_Overflow
   y := (3 * X) / 4;  -- raises Constraint_Error if the machine registers used are 16-bit
  -- mathematically correct intermediate result if 32-bit registers
   Ada.Text_IO.Put ("(");
   Ada.Integer_Text_IO.Put (X);
   Ada.Text_IO.Put (" * 3 ) / 4 = ");
   Ada.Integer_Text_IO.Put (Y);
exception
   when Constraint_Error =>
      Ada.Text_IO.Put_Line ("3 * X too big for register!");
end Demo_Overflow;

基本原理

[edit | edit source]

Ada 语言不要求实现对表达式中的子表达式进行范围检查。Ada 确实要求执行溢出检查。因此,根据求值顺序和寄存器的大小,子表达式将要么溢出,要么产生数学上正确的结果。如果发生溢出,您将获得异常 Constraint_Error。即使程序当前目标上的实现不会导致子表达式求值溢出,您的程序也可能被移植到会导致溢出的实现。

关系测试

[edit | edit source]

指南

[edit | edit source]
  • 考虑使用 <= 和 >= 对实值参数进行关系测试,避免使用 <、>、= 和 /= 操作。
  • 在比较和检查小值时,使用类型属性的值。

例子

[edit | edit source]

以下示例测试(1)存储中的绝对“相等性”,(2)计算中的绝对“相等性”,(3)存储中的相对“相等性”以及(4)计算中的相对“相等性”。

abs (X - Y) <= Float_Type'Model_Small                -- (1)
abs (X - Y) <= Float_Type'Base'Model_Small           -- (2)
abs (X - Y) <= abs X * Float_Type'Model_Epsilon      -- (3)
abs (X - Y) <= abs X * Float_Type'Base'Model_Epsilon -- (4)

具体来说,是针对“等于” 0 的情况

abs X <= Float_Type'Model_Small                      -- (1)
abs X <= Float_Type'Base'Model_Small                 -- (2)
abs X <= abs X * Float_Type'Model_Epsilon            -- (3)
abs X <= abs X * Float_Type'Base'Model_Epsilon       -- (4)

基本原理

[edit | edit source]

严格的关系比较(<、>、=、/=)是涉及实数的计算中的一般性问题。由于比较的方式是在模型区间中定义的,因此比较结果可能取决于实现。在模型区间内,如果两个值不是模型数,则比较这两个值的结果是非确定性的。一般来说,您应该测试邻近度而不是相等性,如示例所示。另见 Rationale (1995, §§G.4.1 和 G.4.2)。

类型属性是符号访问 Ada 数值模型实现的主要手段。当通过类型属性访问模型数的特征时,源代码是可移植的。然后,生成的代码将使用任何实现的适当模型数。

虽然 0 从技术上讲不是一个特例,但它经常被忽视,因为它看起来是最简单,因此也是最安全的案例。但实际上,每次进行涉及小值的比较时,您都应该评估情况,以确定哪种技术是合适的。

注释

[edit | edit source]

无论语言如何,实值计算都会有误差。相应的数学运算具有代数性质通常会引起一些混淆。本指南解释了 Ada 如何解决大多数语言面临的问题。

十进制类型和信息系统附件

[edit | edit source]

指南

[edit | edit source]
  • 在信息系统中,声明不同的数值十进制类型以对应不同的比例(Brosgol、Eachus 和 Emery 1994)。
  • 创建不同十进制类型的对象以反映不同的度量单位(Brosgol、Eachus 和 Emery 1994)。
  • 声明适当比例的十进制类型的子类型,为特定于应用程序的类型提供适当的范围约束。
  • 将每个度量类别封装在包中(Brosgol、Eachus 和 Emery 1994)。
  • 对于无量纲数据,尽可能少地声明十进制类型(Brosgol、Eachus 和 Emery 1994)。
  • 对于十进制计算,确定结果应该是截断到 0 还是四舍五入。
  • 对于不支持信息系统附件(Ada 参考手册 1995,附件 F)的完整功能的编译器,避免使用十进制类型和算术运算。

例子

[edit | edit source]
-- The salary cap today is $500,000; however this can be expanded to $99,999,999.99.
type Executive_Salary is delta 0.01 digits 10 range 0 .. 500_000.00;

------------------------------------------------------------------------------
package Currency is

   type Dollars is delta 0.01 digits 12;

   type Marks   is delta 0.01 digits 12;

   type Yen     is delta 0.01 digits 12;

   function To_Dollars (M : Marks) return Dollars;
   function To_Dollars (Y : Yen)   return Dollars;

   function To_Marks (D : Dollars) return Marks;
   function To_Marks (Y : Yen)     return Marks;

   function To_Yen (D : Dollars) return Yen;
   function To_Yen (M : Marks)   return Yen;

end Currency;

基本原理

[edit | edit source]

Ada 语言没有提供任何预定义的十进制类型。因此,您需要为将要使用的不同比例声明十进制类型。在决定是否使用通用类型时,必须考虑比例和精度的差异(Brosgol、Eachus 和 Emery 1994)。

您需要不同类型的对象,这些对象以不同的单位进行测量。这使编译器能够检测表达式中不匹配的值。如果您将所有十进制对象声明为单个类型,则您将失去强类型的优势。例如,在涉及多种货币的应用程序中,每种货币都应该声明为单独的类型。您应该在不同货币之间提供适当的转换。

您应该将没有特定度量单位的数据映射到一组类型或单个类型,以避免数值类型之间转换的激增。

将十进制类型的范围要求与其精度(即所需的有效位数)分开。从计划变更和易于维护的角度来看,您可以使用数字的值来适应将来存储在类型对象中的值的增长。例如,您可能希望预测数据库值和报表格式的增长。您可以通过与当前需求匹配的范围约束来约束类型的范围。修改范围并避免重新定义数据库和报表更容易。

Ada 会自动截断到 0。如果您的要求是对十进制结果进行四舍五入,则您必须使用 'Round 属性显式地执行此操作。

核心语言定义了十进制类型基本语法和操作。但是,它没有指定必须支持的最小有效数字数量。核心语言也不要求编译器支持 Small 的值,除了 2 的幂,从而使编译器能够有效地拒绝十进制声明(Ada 参考手册 1995,第 3.5.9 节 [注释])。信息系统附录为十进制类型提供了额外的支持。它要求至少 18 位有效数字。它还指定了一个 Text_IO.Editing 包,它提供类似于 COBOL 图片方法的支持。

存储控制

[edit | edit source]

动态存储的管理在不同的 Ada 环境中可能有所不同。事实上,有些环境不提供任何释放机制。以下 Ada 存储控制机制是实现相关的,在编写可移植程序时应该谨慎使用。

表示子句

[edit | edit source]

指南

[edit | edit source]
  • 不要使用表示子句来指定存储单元的数量。

基本原理

[edit | edit source]

'Storage_Size 属性的含义不明确;指定特定值不会提高可移植性。它可能包括或不包括为参数、数据等分配的空间。将此功能的使用保留用于必须依赖特定供应商实现的设计。

注释

[edit | edit source]

在移植活动期间,可以假设任何存储规范的出现都表明必须重新设计的实现依赖性。

访问子程序的值

[edit | edit source]

指南

[edit | edit source]
  • 不要比较访问子程序的值。

基本原理

[edit | edit source]

Ada 参考手册 1995,第 3.10.2 节 [注释] 解释说,“实现可能会认为两个访问子程序的值是不相等的,即使它们指定了同一个子程序。这是因为一个可能直接指向子程序,而另一个可能指向一个执行 Elaboration_Check 然后跳转到子程序的特殊序言。” Ada 参考手册 1995,第 4.5.2 节 [注释] 指出,“对于两个访问值,即使它们指定了同一个子程序,但它们是 Access 属性引用的不同评估结果,它们是相等还是不相等是未指定的。”

另请参阅指南 5.3.4。

异常

[edit | edit source]

如果你必须比较访问子程序的值,你应该使用访问子程序的值定义一个常量,并将所有未来的比较都与该常量进行比较。但是,如果你试图比较不同间接级别上的访问子程序的值,即使它们指定了同一个子程序,这些值也可能仍然不相等。

存储池机制

[edit | edit source]

指南

[edit | edit source]
  • 考虑使用显式定义的存储池机制。

例子

[edit | edit source]

请参阅 Ada 参考手册 1995,第 13.11.2 节 [注释]

你像以前一样使用分配器。不是使用无检查释放,而是维护你自己的空闲对象列表,这些对象不再使用并且可以重复使用。

你使用分配器,可能也使用无检查释放;但是,你实现了一个存储池,并通过 Storage_Pool 子句将其与访问类型相关联。你可以使用此技术来实现一个标记/释放存储管理范例,这可能比分配/释放范例快得多。一些供应商可能会在其 Ada 环境中提供一个标记/释放包。

你不会使用分配器,而是使用地址的无检查转换,并完成你自己的默认初始化等。你不太可能使用最后一个选项,因为你失去了自动默认初始化。

任务

[edit | edit source]

Ada 语言中对任务的定义将任务模型的许多特征留给了实现者。这允许供应商为目标应用领域做出适当的权衡,但也降低了使用任务功能的设计和代码的可移植性。在某些方面,这种可移植性降低是并发方法的固有特征(参见 Nissen 和 Wallis 1984,37)。关于在分布式目标环境中使用 Ada 任务依赖性的讨论超出了本书的范围。例如,多处理器任务调度、处理器间 rendezvous 以及通过 Calendar 包实现的分布式时间感,这些都受到不同实现之间的差异的影响。有关更多信息,Nissen 和 Wallis (1984) 以及 ARTEWG (1986) 讨论了这些问题,而 Volz 等人 (1985) 只是众多可用的研究文章之一。

如果支持实时系统附录,那么许多并发方面都已完全定义,因此,程序可以依赖于这些特性,同时仍然可以移植到符合实时系统附录的其他实现中。以下部分提供了基于缺少该附录的指南。

任务激活顺序

[edit | edit source]

指南

[edit | edit source]
  • 不要依赖在同一个声明列表中声明的任务对象激活的顺序。

基本原理

[edit | edit source]

Ada 参考手册 1995,第 9.2 节 [注释] 中没有定义任务对象激活的顺序。另请参阅指南 6.1.5。

延迟语句

[edit | edit source]

指南

[edit | edit source]
  • 不要依赖于特定延迟的可实现性(Nissen 和 Wallis 1984)。
  • 永远不要使用任务的执行模式知识来实现定时要求。

基本原理

[edit | edit source]

这方面的理由在指南 6.1.7 中有说明。此外,对延迟语句的处理在不同的实现之间有所不同,从而阻碍了可移植性。

使用任务执行模式知识来实现定时要求是不可移植的。Ada 没有指定底层调度算法,并且不能保证不同系统之间系统时钟滴答声始终精确。因此,当你更改系统时钟时,你的延迟行为也会发生变化。

Package Calendar、Type Duration 和 System.Tick

[编辑 | 编辑源代码]
  • 不要假设 System.Tick 与类型 Duration 之间存在关联(参见指南 6.1.7 和 7.4.2)。

基本原理

[编辑 | 编辑源代码]

这种关联不是必需的,尽管在某些实现中可能存在。

选择语句的求值顺序

[编辑 | 编辑源代码]
  • 不要依赖于求值条件的顺序,也不要依赖于从多个打开的选择分支中进行选择的算法。

基本原理

[编辑 | 编辑源代码]

该语言没有定义这些条件的顺序,因此假设它们是任意的。

任务调度算法

[编辑 | 编辑源代码]
  • 不要假设任务在到达同步点之前会无间断地执行。
  • 使用 pragma Priority 仅区分一般的优先级(参见指南 6.1.6)。

基本原理

[编辑 | 编辑源代码]

Ada 任务模型要求任务仅通过语言提供的显式方法进行同步(即 rendezvous、任务依赖性、pragma Atomic)。调度算法不是由语言定义的,并且可能从时间片轮转到抢占式优先级。一些实现提供了几种用户可以选择用于应用程序的选择。

优先级的数量在不同的实现之间可能有所不同。此外,即使实现使用相同的通用调度算法,相同优先级任务的处理方式在不同的实现之间也可能有所不同。

在实时系统中,通常需要严格控制任务算法以获得所需的性能。例如,航空电子系统通常由循环事件驱动,并有限的异步中断。非抢占式任务模型通常用于在这些应用程序中获得最佳性能。循环执行程序可以在 Ada 中编程,就像从循环到多帧速率到完全异步的调度方案一样(MacLaren 1980),尽管通常需要外部时钟。

  • 避免使用 abort 语句。

基本原理

[编辑 | 编辑源代码]

这在指南 6.3.3 中有说明。此外,abort 语句的处理方式因实现而异,从而阻碍了可移植性。

无保护的共享变量和 pragma Atomic 及 Volatile

[编辑 | 编辑源代码]
  • 不要使用无保护的共享变量。
  • 考虑使用受保护的类型来提供数据同步。
  • 让任务通过 rendezvous 机制进行通信。
  • 不要将无保护的共享变量用作任务同步设备。
  • 考虑使用受保护的对象来封装共享数据。
  • 仅在运行时系统缺陷迫使你这样做时才使用 pragma Atomic 或 Volatile。

参见指南 6.1.1 和 6.1.2。

基本原理

[编辑 | 编辑源代码]

这在指南 6.1.1 和 6.2.4 中有说明。此外,无保护的共享变量的处理方式因实现而异,从而阻碍了可移植性。

在使用预定义异常时,应谨慎行事,因为它们处理方式的某些方面可能因实现而异。当然,必须避免特定于实现的异常。有关异常的更多信息,请参见指南 4.3 和 5.8。有关供应商提供的功能的更多信息,请参见指南 7.1.6。

预定义异常和用户定义异常

[编辑 | 编辑源代码]
  • 不要依赖于引发预定义异常的确切位置。
  • 不要依赖 Ada.Exceptions 的行为超出语言中定义的最小值。

基本原理

[编辑 | 编辑源代码]

Ada 参考手册 1995,第 11 节 [带注释的])指出,在不同的实现之间,针对同一原因的预定义异常可能从不同的位置引发。你将无法区分这些异常。此外,每个预定义异常都与各种条件相关联。针对预定义异常编写的任何异常处理程序都必须准备好处理任何这些条件。

指南 5.6.9 讨论了使用块来定义本地异常处理程序,这些处理程序可以捕获靠近其起源点的异常。

特定于实现的异常

[编辑 | 编辑源代码]
  • 不要引发特定于实现的异常。
  • 将接口包中的特定于实现的异常转换为可见的用户定义异常。

基本原理

[编辑 | 编辑源代码]

无法保证任何特定于实现的异常(无论它们是否来自同一供应商)都可移植到其他实现。不仅名称可能不同,而且触发异常的条件范围也可能不同。

如果您为程序的特定于实现的部分创建接口包,则这些包可以捕获或识别特定于实现的异常,并将它们转换为在规范中声明的用户定义异常。当程序移植时,不要让自己被迫查找和更改为这些异常编写的每个处理程序的名称。

表示子句和依赖于实现的特性

[编辑 | 编辑源代码]

Ada 提供了许多依赖于实现的特性,这些特性允许比高级语言通常提供的对底层硬件体系结构的更多控制和交互。这些机制旨在帮助系统编程和实时编程,以获得更高的效率(例如,通过表示子句获得变量的特定大小和布局)和直接的硬件交互(例如,中断条目),而无需诉诸汇编级编程。考虑到这些特性的目标,您通常必须为使用它们付出可移植性方面的显著代价,这一点并不奇怪。一般来说,如果可移植性是主要目标,请不要使用这些特性。当您必须使用这些特性时,将它们封装在包中,这些包被很好地注释为与特定目标环境接口。本节介绍了各种特性以及它们在可移植性方面的推荐用法。

表示子句

[编辑 | 编辑源代码]
  • 使用不依赖于数据表示的算法,因此不需要表示子句。
  • 在访问或定义接口数据时,或者需要特定表示来实现设计时,请考虑使用表示子句。
  • 不要假设在程序之间共享源文件就能保证这些文件中数据类型的表示相同。

基本原理

[编辑 | 编辑源代码]

在许多情况下,使用表示子句来实现算法很容易,即使它不是必需的。还有一种趋势是记录原始程序员对表示的假设,以便将来参考。但不能保证另一种实现将支持所选择的表示。不必要的表示子句还会混淆移植或维护工作,这些工作必须假设程序员依赖于已记录的表示。

与外部系统和设备的接口是需要表示子句的最常见情况。在设计和移植过程中应评估对 pragma Import 和地址子句的使用,以确定是否需要表示子句。

如果没有表示子句,语言不要求对未更改文件的两次编译导致相同的数据表示。可以改变编译之间表示的事项包括

  • 编译顺序中较早文件的更改
  • 优化策略或级别的更改
  • 编译器版本的更改
  • 实际编译器的更改
  • 系统资源可用性的更改

因此,两个独立链接的程序或分区应只共享其表示明确控制的数据。

在移植过程中,所有表示子句都可以被评估为设计工件或访问接口数据的规范,这些规范可能会随着新实现而更改。

包系统

[编辑 | 编辑源代码]
  • 避免使用包 System 常量,除非在尝试概括其他依赖于机器的构造时。

基本原理

[编辑 | 编辑源代码]

由于此包中的值是由实现提供的,因此使用它们可能会产生意想不到的效果。

如果您必须保证物理记录布局在不同实现之间保持一致,则可以使用其第一个和最后一个比特位置来表达记录字段,如 Ada 参考手册 1995,§13.5.1 [注释] 中所示。应使用静态表达式和命名数字,以便编译器能够根据更早的字段计算每个范围的端点。在这种情况下,可以使用 System.Storage_Unit 使编译器计算命名数字的值,从而实现更高的可移植性。但是,此方法可能不适用于 System.Storage_Unit 的所有值。

请使用包 System 常量来参数化其他依赖于实现的特性(参见 Pappas (1985, §13.7.1)。

机器代码插入

[编辑 | 编辑源代码]
  • 避免机器代码插入。

基本原理

[编辑 | 编辑源代码]

Ada 参考手册 (1995,附录 C) 建议实现机器代码插入的包是可选的。此外,它没有标准化,因此机器代码插入很可能不可移植。事实上,可能两个不同供应商的语法对于相同的目标将有所不同,而低级细节的差异,例如寄存器约定,将阻碍可移植性。

如果必须使用机器代码插入来满足另一个项目需求,请识别和记录可移植性降低的影响。

在使用机器代码插入的例程主体声明区域中,插入注释以解释插入提供哪些功能以及(尤其是)为什么需要插入。通过描述对使用其他更高级构造的尝试失败的原因,注释使用机器代码插入的必要性。

与外国语接口

[编辑 | 编辑源代码]
  • 使用包 Interfaces 及其语言定义的子包,而不是依赖于实现的机制。
  • 考虑使用 pragma Import 而不是对其他语言中的子程序使用访问子程序类型。(最好使用“External_Name =>”参数。)
  • 将所有使用 pragma Import、Export 和 Convention 的子程序隔离到特定于实现的(接口)包主体中。

此示例演示了如何与用 C 编写的以下立方根函数进行接口

double cbrt (double x);
package Math_Utilities is

   Argument_Error : exception;

   function Cube_Root (X : Float) return Float;

   ...

end Math_Utilities;
   
------------------------------------------------------------------------------
with Interfaces.C;
package body Math_Utilities is

   function Cube_Root (X : Float) return Float is

      function C_Cbrt (X : Interfaces.C.Double) return Interfaces.C.Double;
      pragma Import (Convention    => C,
                     Entity        => C_Cbrt,
                     External_Name => "cbrt");

   begin
      if X < 0.0 then
         raise Argument_Error;
      else
         return Float (C_Cbrt (Interfaces.C.Double (X)));
      end if;
   end Cube_Root;

   ...

end Math_Utilities;

基本原理

[编辑 | 编辑源代码]

对于静态接口到其他语言的子程序,pragma Import 提供了一个比访问子程序更好的解决方案,因为它不需要间接寻址。pragma Interface(1983 年 Ada 参考手册)已被 pragma Import、Export 和 Convention 替换。Rationale(1995)的附录 B 讨论了如何在与其他语言接口时结合使用这些 pragma 和访问子程序类型。

特别要注意 pragma Import 的“External_Name =>”和“Link_Name =>”参数之间的区别,这两个参数经常被混淆。External_Name 指定子程序名称,如其他语言(如 C 或 Fortran)的源代码中所示。Link_Name 指定链接器使用的名称。通常,只指定这两个参数中的一个,并且通常 External_Name 是可移植性的首选选择。

访问子程序类型对于在单独的子系统(例如 X Window 系统)中实现回调很有用。

与外语接口的问题很复杂。这些问题包括 pragma 语法差异、将 Ada 链接/绑定到其他语言的约定以及将 Ada 变量映射到外语变量。通过将这些依赖项隐藏在接口包中,可以减少代码修改量。

异常

[edit | edit source]

通常需要与其他语言交互,即使只使用汇编语言来访问某些硬件功能。在这种情况下,清楚地注释接口的要求和限制以及 pragma Import、Export 和 Conventions 的使用。

实现特定的 Pragma 和属性

[edit | edit source]

指南

[edit | edit source]
  • 避免编译器实现者添加的 pragma 和属性。

基本原理

[edit | edit source]

Ada 参考手册 (1995) 允许实现者添加 pragma 和属性来利用特定的硬件架构或软件环境。这些显然比实现者对预定义 pragma 和属性的解释更具实现特定性,因此可移植性更差。但是,Ada 参考手册 (1995) 定义了一组附录,它们对某些特殊需求(即实时系统、分布式系统、信息系统、数值、与外语接口以及安全性和安全性)采取了统一一致的方法。您应该始终优先选择附录中定义的功能,而不是任何供应商定义的 pragma 和属性。

未检查的释放

[edit | edit source]

指南

[edit | edit source]
  • 避免依赖于 Ada.Unchecked_Deallocation(参见指南 5.9.2)。

基本原理

[edit | edit source]

未检查的存储释放机制是用于覆盖分配存储被回收的默认时间的其中一种方法。最早的默认时间是当对象不再可访问时,例如,当控制离开声明访问类型的范围时(此时间之后的确切点取决于实现)。如果在此时之前执行了对存储的任何未检查的释放,则如果尝试访问该对象,可能会导致 Ada 程序出错。

本指南比指南 5.9.2 更严格,因为对 Ada.Unchecked_Deallocation 的实现具有极强的依赖性。使用它可能会导致可移植性方面出现相当大的困难。

注释

[edit | edit source]

Ada.Unchecked_Deallocation 是所有 Ada 实现中支持的功能。可移植性问题在于,未检查的存储释放可能会在不同的实现中导致不同的结果。

异常

[edit | edit source]

在对高度迭代或递归算法进行局部控制时,使用未检查的存储释放可能会有益,因为在这种情况下,可用存储可能会超过。

未检查的访问

[edit | edit source]

指南

[edit | edit source]
  • 避免依赖于属性 Unchecked_Access(参见指南 5.9.2)。

基本原理

[edit | edit source]

访问值受可访问性限制。使用属性 Unchecked_Access 会阻止检查这些规则,并且程序员可能会遇到悬挂引用。

未检查的转换

[edit | edit source]

指南

[edit | edit source]
  • 避免依赖于 Ada.Unchecked_Conversion(参见指南 5.9.1)。

基本原理

[edit | edit source]

未检查的类型转换机制实际上是一种绕过 Ada 中强类型设施的方法。实现可以自由限制可以匹配的类型以及当对象大小不同时发生的结果。

异常

[edit | edit source]

未检查的类型转换在 Ada 程序的实现特定部分很有用,在这些部分中,可移植性被隔离,低级编程和外语接口是目标。

如果使用了枚举表示子句,未检查的类型转换是检索枚举值的内部整型代码的唯一语言提供的方式。

运行时依赖项

[edit | edit source]

指南

[edit | edit source]
  • 避免直接调用或隐式依赖底层主机操作系统或 Ada 运行时支持系统,除非该接口在语言中明确定义(例如,Ada 参考手册 [1995] 的附录 C 或 D)。
  • 当您需要调用底层运行时支持系统时,请使用标准绑定和包 Ada.Command_Line。
  • 使用附录中定义的功能,而不是供应商定义的功能。

基本原理

[edit | edit source]

Ada 参考手册 (1995) 中未指定的实现功能通常在不同实现之间会有所不同。特定的实现依赖功能不太可能在其他实现中提供。除了强制性的预定义语言环境外,附录还定义了各种包、属性和编译指示,以规范化几个专门领域的实现依赖功能。当您使用附录中包中声明的功能时,您可以提高可移植性,因为您可以将您的程序移植到实现您所使用相同附录的其他供应商环境中。即使大多数供应商最终提供了类似的功能,他们也不太可能具有相同的公式。事实上,不同的供应商可能使用相同的公式来表示(语义上)完全不同的功能。

在编码时,请尽量避免依赖底层操作系统。考虑在主机开发系统上将系统调用包含在程序中的后果。如果这些调用没有被标记为删除和替换,那么程序可能会经过开发和测试,但当移动到目标环境中时,由于目标环境缺少主机上这些系统调用提供的功能,程序将无法使用。

指南 7.1.5 讨论了 Ada.Command_Line 包的使用。如果 Ada 环境实现了对操作系统服务的标准绑定,例如 POSIX/Ada,并且您编写了符合 POSIX 的调用,那么您的程序应该可以在更多系统之间移植。

在实时嵌入式系统中,调用低级支持系统设施通常是不可避免的。隔离这些设施的使用可能过于困难。对它们进行注释,就像您对机器代码插入进行注释一样(参见指南 7.6.3);在某种程度上,它们是针对支持系统提供的虚拟机提供的指令。当隔离这些功能的使用时,请为您的程序的其他部分提供一个接口,该接口可以通过替换接口的实现来移植。

输入/输出

[编辑 | 编辑源代码]

Ada 中的 I/O 设施不是语言的语法定义的一部分。语言中的构造已被用于为此目的定义一组包。这些包预计不会满足所有应用程序(特别是嵌入式系统)的所有 I/O 需求。它们充当核心子集,可以在简单数据上使用,也可以用作在语言提供的低级构造之上构建 I/O 设施的示例。提供一个能够满足所有应用程序要求并与许多现有操作系统集成的 I/O 定义会导致不可接受的实现依赖性。在与主机操作系统一起运行的应用程序与 Ada 运行时自给自足的嵌入式目标之间,遇到的可移植性问题类型往往有所不同。与主机操作系统交互增加了与主机文件系统结构(例如层次目录)、访问方法(例如索引顺序访问方法 [ISAM])和命名约定(例如基于当前目录的逻辑名称和别名)共存的复杂性。ARTEWG(1986)中的输入/输出部分提供了一些此类依赖性的示例。嵌入式应用程序具有不同的依赖性,这些依赖性通常将它们绑定到其硬件设备的低级细节。

针对 I/O 中这些固有的实现依赖性的主要防御措施是尝试在任何给定应用程序中隔离其功能。以下大多数指南都侧重于此方向。

名称和形式参数

[编辑 | 编辑源代码]
  • 在预定义的 I/O 包上,使用常量和变量作为名称和形式参数的符号实际参数。在实现依赖包中声明和初始化它们。

基本原理

[编辑 | 编辑源代码]

预定义的 I/O 包上这些参数的格式和允许值在不同实现之间可能差异很大。隔离这些值有助于提高可移植性。不指定形式字符串或使用空值并不能保证可移植性,因为实现可以自由指定默认值。

可能希望通过定义其他 Create 和 Open 过程来进一步抽象 I/O 设施,这些过程完全隐藏了 Form 参数的可见性(参见 Pappas 1985,54-55)。

文件关闭

[编辑 | 编辑源代码]
  • 显式关闭所有文件。

基本原理

[编辑 | 编辑源代码]

Ada 参考手册 1995,第 A.7 节 [注释] 没有定义主子程序完成之后外部文件会发生什么情况(特别是如果相应的文件没有被关闭)。

关闭的临时文件的处置可能会有所不同,这可能会影响性能和空间可用性(ARTEWG 1986)。

访问类型上的输入/输出

[编辑 | 编辑源代码]
  • 避免对访问类型执行 I/O。

基本原理

[编辑 | 编辑源代码]

Ada 参考手册 1995,第 A.7 节 [注释] 没有指定对访问类型执行 I/O 的效果。写入此类值时,它将超出实现的范围。因此,它超出了强类型检查的可靠性增强控制的范围。

考虑此操作的含义。访问类型的值的可能实现之一是虚拟地址。如果写入此类值,如何才能期望另一个程序读取该值并对其进行任何合理的利用?该值不能被解释为引用读取器地址空间中的任何有意义的位置,也不能通过读取的值来推断关于写入器地址空间的任何信息。后者与写入器尝试解释或使用该值时会遇到的问题相同,如果该值被读回。也就是说,垃圾收集和/或堆压缩方案可能已移动了以前由该值访问的项目,从而使该值“指向”现在由底层实现以不可确定方式使用的空间。

Ada.Streams.Stream_IO 包

[编辑 | 编辑源代码]
  • 除非您需要 Stream_IO 提供的低级异构 I/O 功能,否则请考虑使用 Sequential_IO 或 Direct_IO 而不是 Stream_IO。

基本原理

[编辑 | 编辑源代码]

Sequential_IO 和 Direct_IO 仍然非常适合处理同构文件。此外,在打算处理同构文件的情况下,使用 Sequential_IO 或 Direct_IO 的好处是可以在编译时强制执行此意图。

Stream_IO 应保留用于处理异构文件。在这种情况下,文件不是所有类型都相同的对象的序列,而是不同类型对象的序列。为了以正确的顺序读取异构对象序列,需要一些特定于应用程序的知识。

当前错误文件

[编辑 | 编辑源代码]
  • 考虑使用 Current_Error 和 Set_Error 来处理运行时错误消息。
with Ada.Text_IO;

...

begin
   Ada.Text_IO.Open (File => Configuration_File,
                     Mode => Ada.Text_IO.In_File,
                     Name => Configuration_File_Name);
exception
   when Ada.Text_IO.Name_Error =>
      Ada.Text_IO.Put_Line (File => Ada.Text_IO.Standard_Error,
                            Item => "Can't open configuration file.");
      ...
end;

基本原理

[编辑 | 编辑源代码]

Text_IO 包包含当前错误文件概念。您应该通过关联的子程序 Current_Error 和 Set_Error 向用户报告错误,而不是使用标准输出工具。在交互式应用程序中,使用 Text_IO 错误工具可以提高用户界面的可移植性。

在具有多个 I/O 任务的程序中,您需要注意多个任务尝试设置 Current_Input、Current_Output 或 Current_Error 的情况。潜在问题在于对与包关联的“共享”状态的无保护更新,在本例中为 Text_IO 包。准则 6.1.1 和 6.2.4 讨论了与无保护共享变量相关的相关问题。

  • 在旨在具有较长生命周期的程序或组件中,避免使用 Ada 参考手册(1995)的附录 J 声明为“过时”的 Ada 特性,除非需要使用该特性才能向后兼容 Ada 83(Ada 参考手册 1983)。
  • 记录任何过时特性的使用情况。
  • 避免使用以下特性
    • 预定义环境中包的简短重命名(例如,Text_IO 而不是 Ada.Text_IO)
    • 对 ! 替代为 |, : 替代为 #,以及 % 替代为引号的字符替换
    • 浮点类型的精度降低的子类型
    • 应用于私有类型的'Constrained 属性
    • 预定义的包 ASCII
    • 异常 Numeric_Error
    • 各种表示规范,包括 at 子句、mod 子句、中断入口和 Storage_Size 属性
  • 对以下内容在潜在目标平台上提供的支持做出明智的假设
    • 整数类型可用位数(范围约束)
    • 浮点类型可用的精度小数位数
    • 定点类型可用位数(增量和范围约束)
    • 源文本每行字符数
    • Root_Integer 表达式位数
    • Duration 范围内的秒数
    • Duration'Small 的毫秒数
    • 十进制类型的最小和最大比例
  • 避免对 Character 类型中包含的值和值的数量做出假设。
  • 对每个包、子程序和任务使用突出显示的注释,其中存在任何不可移植的特性。
  • 对于每个使用的不可移植特性,描述对该特性的期望。
  • 考虑仅使用无参数过程作为主子程序。
  • 考虑使用 Ada.Command_Line 访问环境中的值,但要注意此包的行为甚至其规范都是不可移植的。
  • 封装和记录所有对包 Ada.Command_Line 的使用。
  • 创建专门设计的包,以隔离硬件和实现依赖,并且这些包的规范在移植时不会发生变化。
  • 如果硬件或实现相关的代码是出于机器或解决方案效率的原因,则要明确说明目标。
  • 对于隐藏实现依赖关系的包,为不同的目标环境维护不同的包体。
  • 将中断接收任务隔离到实现相关的包中。
  • 有关实现相关的功能列表,请参见 Ada 参考手册(1995 年)的附件 M。
  • 避免使用供应商提供的包。
  • 避免使用添加到预定义包中的功能,这些功能未在 Ada 语言定义或专门需求附件中指定。
  • 使用专门需求附件中定义的功能,而不是供应商定义的功能。
  • 清楚地记录对来自专门需求附件的任何功能的使用(系统编程、实时系统、分布式系统、信息系统、数值和安全与安全性)。
  • 不要编写其正确执行依赖于实现使用的特定参数传递机制的代码(Ada 参考手册 1995 年,§6.2 [Annotated];Cohen 1986)。
  • 如果子程序具有多个给定子类型的形式参数,其中至少一个为 [in] out,则确保子程序能够正确处理两个形式参数都表示同一实际对象的情况。
  • 避免依赖于 Ada 中某些构造的评估顺序。

数值类型和表达式

[编辑 | 编辑源代码]
  • 避免使用 Standard 包中的预定义数值类型。使用范围和位数声明,并让实现选择合适的表示形式。
  • 对于需要比全局假设提供的精度更高的程序,请定义一个包,该包声明私有类型和操作,具体需求见Pappas(1985),其中有完整解释和示例。
  • 考虑对以下内容使用预定义的数值类型(Integer,Natural,Positive):
    • 数组的索引,其中索引类型并不重要,例如String类型
    • “纯”数字,即没有关联物理单位的数字(例如,指数)
    • 用于控制重复或迭代计数的值
  • 当性能和精度是首要问题时,请使用支持 Numerics 附录 (Ada 参考手册 1995,附录 G) 的实现。
  • 仔细分析您真正需要的精度。
  • 不要逼近机器的精度极限。
  • 注释程序的数值方面的分析和推导。
  • 预测子表达式的值范围,以避免超出其基本类型的底层范围。使用派生类型、子类型、分解和数值类型的范围约束。
  • 考虑使用 <= 和 >= 对实值参数进行关系测试,避免使用 <、>、= 和 /= 操作。
  • 在比较和检查小值时,使用类型属性的值。
  • 在信息系统中,声明不同的数值十进制类型以对应不同的比例(Brosgol、Eachus 和 Emery 1994)。
  • 创建不同十进制类型的对象以反映不同的度量单位(Brosgol、Eachus 和 Emery 1994)。
  • 声明适当比例的十进制类型的子类型,为特定于应用程序的类型提供适当的范围约束。
  • 将每个度量类别封装在包中(Brosgol、Eachus 和 Emery 1994)。
  • 对于无量纲数据,尽可能少地声明十进制类型(Brosgol、Eachus 和 Emery 1994)。
  • 对于十进制计算,确定结果应该是截断到 0 还是四舍五入。
  • 对于不支持信息系统附件(Ada 参考手册 1995,附件 F)的完整功能的编译器,避免使用十进制类型和算术运算。

存储控制

[编辑 | 编辑源代码]
  • 不要使用表示子句来指定存储单元的数量。
  • 不要比较访问子程序的值。
  • 考虑使用显式定义的存储池机制。
  • 不要依赖在同一个声明列表中声明的任务对象激活的顺序。
  • 不要依赖于特定延迟的可实现性(Nissen 和 Wallis 1984)。
  • 永远不要使用任务的执行模式知识来实现定时要求。
  • 不要假设 System.Tick 和 Duration 类型之间存在关联。
  • 不要依赖于求值条件的顺序,也不要依赖于从多个打开的选择分支中进行选择的算法。
  • 不要假设任务在到达同步点之前会无间断地执行。
  • 使用 pragma Priority 仅区分一般重要性级别。
  • 避免使用 abort 语句。
  • 不要使用无保护的共享变量。
  • 考虑使用受保护的类型来提供数据同步。
  • 让任务通过 rendezvous 机制进行通信。
  • 不要将无保护的共享变量用作任务同步设备。
  • 考虑使用受保护的对象来封装共享数据。
  • 仅在运行时系统缺陷迫使你这样做时才使用 pragma Atomic 或 Volatile。
  • 不要依赖于引发预定义异常的确切位置。
  • 不要依赖 Ada.Exceptions 的行为超出语言中定义的最小值。
  • 不要引发特定于实现的异常。
  • 将接口包中的特定于实现的异常转换为可见的用户定义异常。

表示子句和实现相关的特性

[编辑 | 编辑源代码]
  • 使用不依赖于数据表示的算法,因此不需要表示子句。
  • 访问或定义接口数据时,或需要特定表示形式来实现设计时,请考虑使用表示子句。
  • 不要假设在程序之间共享源文件就能保证这些文件中数据类型的表示相同。
  • 避免使用包 System 常量,除非在尝试概括其他依赖于机器的构造时。
  • 避免机器代码插入。
  • 使用包 Interfaces 及其语言定义的子包,而不是依赖于实现的机制。
  • 考虑使用 pragma Import 而不是指向子程序类型的访问,以便与其他语言中的子程序进行接口。
  • 将所有使用 pragma Import、Export 和 Convention 的子程序隔离到特定于实现的(接口)包主体中。
  • 避免编译器实现者添加的 pragma 和属性。
  • 避免依赖 Ada.Unchecked_Deallocation。
  • 避免依赖 Unchecked_Access 属性。
  • 避免依赖 Ada.Unchecked_Conversion。
  • 避免直接调用或隐式依赖底层主机操作系统或 Ada 运行时支持系统,除非该接口在语言中明确定义(例如,Ada 参考手册 [1995] 的附录 C 或 D)。
  • 当您需要调用底层运行时支持系统时,请使用标准绑定和 Ada.Command_Line 包。

运行时支持系统。

  • 使用附录中定义的功能,而不是供应商定义的功能。

输入/输出

[编辑 | 编辑源代码]
  • 在预定义的 I/O 包上,使用常量和变量作为名称和形式参数的符号实际参数。在实现依赖包中声明和初始化它们。
  • 显式关闭所有文件。
  • 避免对访问类型执行 I/O。
  • 除非您需要 Stream_IO 提供的低级异构 I/O 功能,否则请考虑使用 Sequential_IO 或 Direct_IO 而不是 Stream_IO。
  • 考虑使用 Current_Error 和 Set_Error 来处理运行时错误消息。

可重用性

华夏公益教科书