跳转到内容

Ada 样式指南/编程实践

来自维基教科书,开放世界中的开放书籍

程序结构 · 并发

软件总是处于变化之中。这种变化的需求,委婉地说叫做“维护”,来自各种来源。错误需要在发现时被纠正。系统功能可能需要以计划或非计划的方式进行增强。不可避免的是,在系统的整个生命周期中,需求都会发生变化,迫使系统不断演进。通常,这些修改是在软件最初编写很久之后进行的,通常由除最初编写者之外的人进行。

轻松、成功的修改要求软件可读、易懂,并按照既定的实践进行结构化。如果一个软件组件不能被熟悉其预期功能的程序员轻易理解,那么该软件组件就不是可维护的。使代码可读和易懂的技术可以提高其可维护性。前面的章节介绍了诸如一致地使用命名约定、清晰且组织良好的注释以及适当的模块化等技术。本章介绍了对语言特性的持续使用和逻辑使用。

正确性是可靠性的一个方面。虽然样式指南无法强制使用正确的算法,但它们可以建议使用已知可以减少错误数量或可能性或通过定义错误发生时的行为来提高程序可预测性的技术和语言特性。这些技术包括可以减少错误可能性或通过定义错误发生时的行为来提高程序可预测性的程序构造方法。


语法的可选部分

[编辑 | 编辑源代码]

Ada 语法中的部分内容虽然是可选的,但可以增强代码的可读性。以下给出的指南涉及对其中一些可选功能的使用。

循环名称

[编辑 | 编辑源代码]
  • 当循环嵌套时,将名称与循环关联(Booch 1986、1987)。
  • 将名称与包含任何exit语句的循环关联。
Process_Each_Page:
   loop
      Process_All_The_Lines_On_This_Page:
         loop
            ...
            exit Process_All_The_Lines_On_This_Page when Line_Number = Max_Lines_On_Page;
            ...
            Look_For_Sentinel_Value:
               loop
                  ...
                  exit Look_For_Sentinel_Value when Current_Symbol = Sentinel;
                  ...
               end loop Look_For_Sentinel_Value;
            ...
         end loop Process_All_The_Lines_On_This_Page;
      ...
      exit Process_Each_Page when Page_Number = Maximum_Pages;
      ...
   end loop Process_Each_Page;


当您将名称与循环关联时,您必须将该名称与该循环的关联

endexit一起包含(Ada 参考手册 1995)。这有助于读者找到任何给定循环的关联

end


。这在循环跨越屏幕或页面边界时尤其重要。为循环选择一个好名字可以记录其目的,减少对解释性注释的需求。如果为循环选择名称非常困难,这可能表明需要对算法进行更多思考。

定期为循环命名有助于您遵循指南 5.1.3。即使在代码发生更改的情况下,例如,添加外部或内部循环,

指南

end
  • 语句也不会变得模棱两可。

示例

可能很难为每个循环想出一个名称;因此,指南指定了嵌套循环。在可读性和二次思考方面的益处超过了为循环命名带来的不便。
Trip:
   declare
      ...
   begin  -- Trip
      Arrive_At_Airport:
         declare
            ...
         begin  -- Arrive_At_Airport
            Rent_Car;
            Claim_Baggage;
            Reserve_Hotel;
            ...
         end Arrive_At_Airport;
      Visit_Customer:
         declare
            ...
         begin  -- Visit_Customer
            -- again a set of activities...
            ...
         end Visit_Customer;
      Departure_Preparation:
         declare
            ...
         begin  -- Departure_Preparation
            Return_Car;
            Check_Baggage;
            Wait_For_Flight;
            ...
         end Departure_Preparation;
      Board_Return_Flight;
   end Trip;

原理

块名称

[编辑 | 编辑源代码][编辑 | 编辑源代码]当块嵌套时,将名称与块关联。

[编辑 | 编辑源代码]

[编辑 | 编辑源代码]


当存在嵌套块结构时,可能难以确定哪个

[编辑 | 编辑源代码]
  • 在所有exit来自嵌套循环的语句中使用循环名称。

参见 5.1.1 中的示例。


一个exit语句是一个隐式的goto. 它应该明确地指定其源。当存在嵌套循环结构并且一个exit语句被使用时,可能很难确定退出的是哪个循环。此外,将来可能引入嵌套循环的更改很可能会引入错误,因为exit意外地从错误的循环中退出。命名循环及其退出将减轻这种混乱。如果嵌套循环跨越屏幕或页面边界,此指南也很有用。


命名 End 语句

[编辑 | 编辑源代码]
  • 在包规范和主体末尾包含定义的程序单元名称。
  • 在任务规范和主体末尾包含定义的标识符。
  • accept语句的循环关联。
  • 末尾包含入口标识符。
  • 在子程序主体末尾包含设计器。

示例

在受保护单元声明末尾包含定义的标识符。
------------------------------------------------------------------------
package Autopilot is
   function Is_Engaged return Boolean;
   procedure Engage;
   procedure Disengage;
end Autopilot;
------------------------------------------------------------------------
package body Autopilot is
   ...
   ---------------------------------------------------------------------
   task Course_Monitor is
      entry Reset (Engage : in     Boolean);
   end Course_Monitor;
   ---------------------------------------------------------------------
   function Is_Engaged return Boolean is
   ...
   end Is_Engaged;
   ---------------------------------------------------------------------
   procedure Engage is
   ...
   end Engage;
   ---------------------------------------------------------------------
   procedure Disengage is
   ...
   end Disengage;
   ---------------------------------------------------------------------
   task body Course_Monitor is
   ...
         accept Reset (Engage : in     Boolean) do
            ...
         end Reset;
   ...
   end Course_Monitor;
   ---------------------------------------------------------------------
end Autopilot;
------------------------------------------------------------------------


[编辑 | 编辑源代码][编辑 | 编辑源代码]在这些复合语句末尾重复名称可确保代码的一致性。此外,命名的


如果单元跨越页面或屏幕边界,或者如果它包含嵌套单元,则为读者提供参考。

参数列表

[编辑 | 编辑源代码]


子程序或入口参数列表是子程序或入口实现的抽象的接口。重要的是它清晰且以一致的风格表达。关于形式参数命名和排序的慎重决定可以使子程序的目的更容易理解,从而可以使子程序更容易使用。

形式参数

示例

用描述性的名称命名形式参数,以减少对注释的需求。
List_Manager.Insert (Element     => New_Employee,
                     Into_List   => Probationary_Employees,
                     At_Position => 1);

[编辑 | 编辑源代码]


遵循形式参数的变量命名指南 ( 3.2.1 和 3.2.3 ) 可以使对子程序的调用更像普通散文,如上面的示例所示,其中不需要任何注释。这种类型的描述性名称还可以使子程序主体中的代码更清晰。

命名关联
  • [编辑 | 编辑源代码]
  • 在很少使用或具有许多形式参数的子程序或入口的调用中使用命名参数关联。
  • 在实例化泛型时使用命名关联。
  • 当实际参数是任何字面量或表达式时,使用命名关联进行澄清。

为可选参数提供非默认值时使用命名关联。

实例化

示例

在从单个源文件中的不到五个地方调用或具有超过两个形式参数的子程序或入口的调用中使用命名参数关联。
Encode_Telemetry_Packet (Source         => Power_Electronics,
                         Content        => Temperature,
                         Value          => Read_Temperature_Sensor(Power_Electronics),
                         Time           => Current_Time,
                         Sequence       => Next_Packet_ID,
                         Vehicle        => This_Spacecraft,
                         Primary_Module => True);

[编辑 | 编辑源代码]

很少使用或具有许多形式参数的子程序或入口的调用,如果不参考子程序或入口代码就可能难以理解。命名参数关联可以使这些调用更具可读性。

当形式参数已适当地命名时,可以更轻松地确定子程序的确切用途,而无需查看其代码。这减少了仅为使调用更具可读性而存在的命名常量的需求。它还允许用作实际参数的变量被赋予指示其用途的名称,而无需考虑它们在调用中传递的原因。实际参数(是表达式而不是变量)不能以其他方式命名。


命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

笔记

[编辑 | 编辑源代码]


命名参数关联是否提高可读性的判断是主观的。当然,简单的或熟悉的子程序,例如交换例程或正弦函数,不需要在过程调用中使用命名关联的额外说明。

警告

[编辑 | 编辑源代码]


命名参数关联的结果是,形式参数名称可能无法在不修改每个调用文本的情况下更改。

默认参数

指南

[edit | edit source]
  • 提供默认参数,以便偶尔对广泛使用的子程序或条目进行特殊使用。
  • 将默认参数放在形式参数列表的末尾。
  • 考虑为添加到现有子程序的新参数提供默认值。

示例

[edit | edit source]

Ada 参考手册 (1995) 包含许多关于这种实践的示例。


原理

[edit | edit source]

通常,子程序或条目的大多数使用都需要对给定参数使用相同的值。提供该值作为参数的默认值,会使参数在大多数调用中成为可选的。它还允许剩余的调用通过为该参数提供不同的值来自定义子程序或条目。

将默认参数放在形式参数列表的末尾,允许调用者在调用时使用位置关联;否则,只有在使用命名关联时,默认值才可用。

通常在维护活动期间,您会增加子程序或条目的功能。这需要比原始形式为某些调用提供更多参数。可能需要新的参数来控制此新功能。为新参数提供指定旧功能的默认值。需要旧功能的调用不需要更改;它们采用默认值。如果将新参数添加到参数列表的末尾,或者在所有调用中使用命名关联,则情况也是如此。需要新功能的新调用可以通过为新参数提供其他值来指定该功能。

这提高了可维护性,因为使用修改后的例程的位置本身不需要修改,而例程的先前功能级别可以“重用”。


例外情况

[edit | edit source]

不要过度。如果功能上的变化非常激进,那么您应该准备一个单独的例程,而不是修改现有的例程。这种情况的一个指标是难以确定用于默认值的价值组合,这些组合唯一且自然地需要两种功能中更严格的一种。在这种情况下,最好继续创建单独的例程。


模式指示

[edit | edit source]

指南

[edit | edit source]
  • 显示所有过程和条目参数的模式指示(Nissen 和 Wallis 1984)。
  • 使用适用于您的应用程序的最严格的参数模式。

示例

[edit | edit source]
procedure Open_File (File_Name   : in     String;
                     Open_Status :    out Status_Codes);
entry Acquire (Key      : in     Capability;
               Resource :    out Tape_Drive);

原理

[edit | edit source]

通过显示参数的模式,您可以帮助阅读者。如果您没有指定参数模式,则默认模式为in。明确显示所有参数的模式指示比仅仅使用默认模式是一种更肯定的操作。任何以后审查代码的人都会更有信心,您希望参数模式为in.

使用反映参数实际使用的模式。您应该避免将所有参数都设置为in out模式的倾向,因为out模式参数既可以检查也可以更新。


例外情况

[edit | edit source]

可能需要考虑给定抽象的几种替代实现。例如,有界堆栈可以实现为指向数组的指针。即使对被指向对象的更新不需要更改指针值本身,您可能也希望考虑将模式设为in out以允许对实现进行更改,并更准确地记录操作正在执行的操作。如果您以后将实现更改为简单数组,则模式将必须为in out,这可能会导致对调用例程的所有位置进行更改。


类型

[edit | edit source]

除了确定变量和子类型名称的可能值外,类型区分还可以成为开发安全、可读和易于理解代码的宝贵工具。类型阐明了数据的结构,可以限制或限制对该数据执行的操作。 “保持类型区分已被证明是发现程序编写时逻辑错误的非常有效的手段,并在程序随后维护时提供宝贵的帮助”(Pyle 1985)。利用 Ada 的强类型功能,例如子类型、派生类型、任务类型、受保护类型、私有类型和受限私有类型。

这些指南鼓励编写大量代码以确保强类型。虽然可能看起来这种代码量会带来执行性能上的损失,但实际上通常并非如此。与其他传统语言不同,Ada 在编写的代码量与生成的执行程序的大小之间没有直接的关系。大多数强类型检查是在编译时而不是执行时执行的,因此执行代码的大小不会受到很大影响。

有关特定类型的数据结构和标记类型的指南,请分别参见 9.2.1。


派生类型和子类型

[edit | edit source]

指南

[edit | edit source]
  • 通过从现有类型派生新类型来使用现有类型作为构建块。
  • 对子类型使用范围约束。
  • 定义新类型,尤其是派生类型,以包含最大可能的价值集合,包括边界值。
  • 使用子类型限制派生类型的范围,排除边界值。
  • 当没有有意义的组件添加到类型时,使用类型派生而不是类型扩展。

示例

[edit | edit source]

类型Table是创建新类型的构建块

type Table is
   record
      Count : List_Size  := Empty;
      List  : Entry_List := Empty_List;
   end record;
type Telephone_Directory  is new Table;
type Department_Inventory is new Table;

以下是不能在未明确编程使用它们的运算中混合使用的不同类型

type Dollars is new Number;
type Cents   is new Number;

下面,Source_Tail的值在Listing_Paper的范围内,当行为空时。只要结果落在正确的子类型范围内,所有索引都可以在表达式中混合使用

type Columns          is range First_Column - 1 .. Listing_Width + 1;

subtype Listing_Paper is Columns range First_Column .. Listing_Width;
subtype Dumb_Terminal is Columns range First_Column .. Dumb_Terminal_Width;
type Line             is array (Columns range <>) of Bytes;
subtype Listing_Line  is Line (Listing_Paper);
subtype Terminal_Line is Line (Dumb_Terminal);
Source_Tail : Columns       := Columns'First;
Source      : Listing_Line;
Destination : Terminal_Line;
...
Destination(Destination'First .. Source_Tail - Destination'Last) :=
      Source(Columns'Succ(Destination'Last) .. Source_Tail);


原理

[edit | edit source]

派生类型的名称可以清楚地表明其预期用途,并避免类似类型定义的激增。两个派生类型的对象,即使派生自相同的类型,也不能在操作中混合使用,除非明确提供这些操作,或者其中一个被明确转换为另一个。这种禁止是强类型的强制执行。

谨慎而有意地定义新类型、派生类型和子类型。子类型和派生类型不是等效的概念,但它们可以协同使用以获得优势。子类型限制了类型可能值的范围,但没有定义新类型。

类型可以具有高度受限的值集,而不会消除有用的值。协同使用派生类型和子类型可以消除可执行语句中的许多标志变量和类型转换。这使程序更具可读性,强制执行抽象,并允许编译器强制执行强类型约束。

许多算法以正常范围之外的值开始或结束。如果边界值在子表达式中不兼容,算法可能会变得不必要地复杂。当程序可以简单地测试零或其他哨兵值(位于正常范围之外)时,程序可能会因标志变量和特殊情况而变得杂乱无章。

类型Columns和子类型Listing_Paper在上面的示例中演示了如何允许哨兵值。子类型Listing_Paper可以用作在包规范中声明的子程序参数的类型。这将限制调用者可以指定的值得范围。同时,类型Columns可以用作在包体中内部存储此类值,允许First_Column - 1用作哨兵值。这种类型和子类型的组合允许子表达式中的子类型之间兼容,而无需类型转换(就像使用派生类型时那样)。

类型派生和类型扩展之间的选择取决于您希望对类型中的对象进行何种更改。一般来说,类型派生是一种非常简单的继承形式:派生类型继承了父类型的结构、操作和值(Rationale 1995,§4.2)。虽然您可以添加操作,但不能扩充数据结构。您可以从标量类型或复合类型派生。

类型扩展是一种更强大的继承形式,仅适用于标记记录,您可以在其中扩充类型的组件和操作。当记录实现具有重用和/或扩展潜力的抽象时,它是将其设置为标记的良好候选者。类似地,如果抽象是具有明确定义变量和通用属性的抽象系列的成员,则应考虑使用标记记录。


命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[edit | edit source]

减少独立类型声明数量的代价是,当基本类型重新定义时,子类型和派生类型会发生变化。这种级联变化有时是福,有时是祸。但是,通常它是有意且有益的。


匿名类型

[edit | edit source]

指南

[edit | edit source]
  • 避免使用匿名数组类型。
  • 仅当不存在或无法创建合适的类型,并且数组不会被整体引用(例如,用作子程序参数)时,才为数组变量使用匿名数组类型。
  • 使用访问参数和访问判别式来确保参数或判别式被视为常量。

示例

[edit | edit source]

使用

type Buffer_Index is range 1 .. 80;
type Buffer       is array (Buffer_Index) of Character;
Input_Line : Buffer;

而不是

Input_Line : array (Buffer_Index) of Character;


原理

[edit | edit source]

虽然 Ada 允许匿名类型,但它们的使用有限,会使程序修改变得复杂。例如,除了数组之外,匿名类型的变量永远不能用作实际参数,因为不可能定义相同类型的形式参数。即使这可能不是最初的限制,它也排除了将代码段更改为子程序的修改。虽然您可以将匿名数组声明为别名,但您不能将此访问值用作子程序中的实际参数,因为子程序的形式参数声明需要类型标记。此外,使用相同的匿名类型声明声明的两个变量实际上是不同类型的。

即使 Ada 支持参数传递期间的数组类型隐式转换,也很难证明不使用参数类型。在大多数情况下,参数类型是可见的,并且可以轻松地替代匿名数组类型。使用匿名数组类型意味着数组仅用作实现值集合的便捷方式。使用匿名类型,然后将变量视为对象,这是具有误导性的。

当您使用访问参数或访问判别式时,匿名类型本质上是在子程序或对象本身内声明的(Rationale 1995,§3.7.1)。因此,您无法声明相同类型的其他对象,并且对象被视为常量。对于自引用数据结构(参见指南 5.4.6),您需要访问参数才能操作判别式访问的数据(Rationale 1995,§3.7.1)。


命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[edit | edit source]

有关匿名任务类型,请参见指南 6.1.4。


例外情况

[edit | edit source]

如果您要创建一个唯一的表,例如元素周期表,请考虑使用匿名数组类型。


私有类型

[edit | edit source]

指南

[edit | edit source]
  • 优先从受控类型派生,而不是使用受限私有类型。
  • 优先使用受限私有类型,而不是私有类型。
  • 优先使用私有类型,而不是非私有类型。
  • 显式导出所需的操作,而不是放宽限制。

示例

[edit | edit source]
------------------------------------------------------------------------
with Ada.Finalization;
package Packet_Telemetry is
   type Frame_Header is new Ada.Finalization.Controlled with private;
   type Frame_Data   is private;
   type Frame_Codes  is (Main_Bus_Voltage, Transmitter_1_Power);
   ...
private
   type Frame_Header is new Ada.Finalization.Controlled with
      record
         ...
      end record;
   -- override adjustment and finalization to get correct assignment semantics
   procedure Adjust (Object : in out Frame_Header);
   procedure Finalize (Object : in out Frame_Header);
   type Frame_Data is
      record
         ...
      end record;
   ...
end Packet_Telemetry;
------------------------------------------------------------------------


原理

[edit | edit source]

受限私有类型和私有类型比非私有类型更好地支持抽象和信息隐藏。类型越受限制,信息隐藏就越好。反过来,这使得实现能够更改,而不会影响程序的其余部分。虽然有许多正当理由导出类型,但最好先尝试首选路线,仅在必要时放宽限制。如果包的用户需要使用几个受限操作,最好通过导出的子程序显式地单独导出操作,而不是降低限制级别。这种做法保留了其他操作的限制。

受限私有类型具有一组最受限制的操作,这些操作可供包的用户使用。在必须提供给包用户的类型中,尽可能多地应从受控类型或受限私有类型派生。受控类型使您能够调整赋值和最终确定值,因此您不再需要创建受限私有类型来保证客户端赋值和相等性服从深层复制/比较语义。因此,可以导出一个稍微不太受限制的类型(即扩展Ada.Finalization.Controlled)的私有类型,它具有可调整的赋值运算符和可覆盖的相等运算符。另请参见指南 5.4.5。

受限私有类型可供用户使用的操作是成员测试、选定组件、任何判别式的选择组件、限定和显式转换,以及属性'Base'Size. 受限私有类型的对象还具有属性'Constrained(如果有判别式)。这些操作都不允许包用户以依赖类型结构的方式操作对象。


命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[edit | edit source]

预定义包Direct_IOSequential_IO不接受有限私有类型作为泛型参数。在需要对类型进行 I/O 操作时,应考虑此限制。

有关在泛型单元中使用私有类型和有限私有类型的讨论,请参见指南 8.3.3。


子程序访问类型

[编辑 | 编辑源代码]
  • 使用访问子程序类型来间接访问子程序。
  • 在可能的情况下,使用抽象标记类型和分派,而不是访问子程序类型来实现子程序的动态选择和调用。

以下示例摘自《原理》(1995 年,第 3.7.2 节)

generic
   type Float_Type is digits <>;
package Generic_Integration is
   type Integrand is access function (X : Float_Type) return Float_Type;
   function Integrate (F        : Integrand;
                       From     : Float_Type;
                       To       : Float_Type;
                       Accuracy : Float_Type := 10.0*Float_Type'Model_Epsilon)
     return Float_Type;
end Generic_Integration;
with Generic_Integration;
procedure Try_Estimate (External_Data : in     Data_Type;
                        Lower         : in     Float;
                        Upper         : in     Float;
                        Answer        :    out Float) is
   -- external data set by other means
   function Residue (X : Float) return Float is
      Result : Float;
   begin  -- Residue
      -- compute function value dependent upon external data
      return Result;
   end Residue;
   package Float_Integration is
      new Generic_Integration (Float_Type => Float);

   use Float_Integration;
begin -- Try_Estimate
   ...
   Answer := Integrate (F    => Residue'Access,
                        From => Lower,
                        To   => Upper);
end Try_Estimate;


访问子程序类型允许您创建包含子程序引用的数据结构。此功能有许多用途,例如实现状态机、X 窗口系统中的回调、迭代器(应用于列表中每个元素的操作)以及数值算法(例如积分函数)(《原理》,1995 年,第 3.7.2 节)。

您可以通过使用抽象标记类型来实现与访问子程序类型相同的动态选择效果。您可以声明一个带有一个抽象操作的抽象类型,然后使用访问类范围类型来获得分派效果。与访问子程序类型相比,这种技术提供了更大的灵活性和类型安全性(Ada 语言参考手册 1995 年,第 3.10.2 节 [注释])。

访问子程序类型在实现动态选择方面很有用。子程序的引用可以直接存储在数据结构中。例如,在有限状态机中,一个数据结构可以描述在状态转换时要采取的操作。由于 Ada 95 要求指定的子程序具有与子程序访问类型中指定的参数/结果配置文件相同的配置文件,因此可以维护强类型检查。

另请参见指南 7.3.2。


数据结构

[编辑 | 编辑源代码]

Ada 的数据结构化功能是一种强大的资源;因此,使用它们尽可能地对数据进行建模。可以对逻辑相关的组数据,并让语言控制数据的抽象和操作,而不是要求程序员或维护人员这样做。数据也可以以构建块的方式组织。除了显示数据结构的组织方式(并可能向读者暗示为什么以这种方式组织)之外,从更小的组件创建数据结构还允许重用这些组件。使用 Ada 提供的功能可以提高代码的可维护性。


带判别的记录

[编辑 | 编辑源代码]
  • 在声明判别式时,使用尽可能受约束的子类型(即,具有尽可能具体的范围约束的子类型)。
  • 使用带判别的记录,而不是受约束的数组来表示实际值不受约束的数组。

类型为Name_Holder_1的对象可能包含长度为Natural'Last:

type Number_List is array (Integer range <>) of Integer;

type Number_Holder_1 (Current_Length : Natural := 0) is
   record
      Numbers : Number_List (1 .. Current_Length);
   end record;

类型为的字符串Name_Holder_2

type    Number_List is array (Integer range <>) of Integer;
subtype Max_Numbers is Natural range 0 .. 42;

type Number_Holder_2 (Current_Length : Max_Numbers := 0) is
   record
      Numbers : Number_List (1 .. Current_Length);
   end record;

原理

对字符串组件的长度施加了更合理的限制

[编辑 | 编辑源代码]

当您使用判别式来约束带判别的记录内的数组时,判别式可以取值的范围越大,类型的对象可能需要的空间就越大。尽管您的程序可以编译和链接,但当运行时系统无法创建所需潜在大小的对象时,它会在执行时失败。带判别的记录捕获了边界在运行时可能变化的数组的意图。简单的受约束的数组定义(例如,type Number_List is array (1 .. 42) of Integer;

异构相关数据

示例

考虑将记录映射到 I/O 设备数据。
type Propulsion_Method is (Sail, Diesel, Nuclear);
type Craft is
   record
      Name   : Common_Name;
      Plant  : Propulsion_Method;
      Length : Feet;
      Beam   : Feet;
      Draft  : Feet;
   end record;
type Fleet is array (1 .. Fleet_Size) of Craft;

[编辑 | 编辑源代码]

通过将相关数据收集到同一个构造中,您可以帮助维护人员找到所有相关数据,简化对所有数据而不是部分数据的任何修改。这反过来又会提高可靠性。您或未知的维护人员都不太可能忘记在可执行语句中处理所有信息片段,尤其是在尽可能使用聚合赋值进行更新的情况下。想法是将维护人员需要了解的信息放在最容易找到的地方。例如,如果与给定Craft

相关的所有信息都位于同一个地方,那么这种关系在声明中以及在访问和更新该信息的代码中就非常清楚。但是,如果它分散在几个数据结构中,那么就不太明显这是有意关系还是偶然关系。在后一种情况下,声明可以分组在一起以暗示意图,但可能无法以这种方式分组访问和更新代码。确保使用相同的索引访问几个并行数组中的对应元素非常困难,尤其是在访问分散的情况下。


命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

如果应用程序必须直接与硬件接口,则使用记录,尤其是在与记录表示子句结合使用时,可能有助于映射到相关硬件的布局。

[编辑 | 编辑源代码]


例外情况

将异构数据存储在并行数组中,这相当于 FORTRAN 风格,似乎是可取的。这种风格是 FORTRAN 数据结构化限制的产物。FORTRAN 只提供构建同构数组的功能。

[编辑 | 编辑源代码]


如果应用程序必须直接与硬件接口,并且硬件要求信息分布在各个位置,则可能无法使用记录。

异构多态数据
  • 使用访问类型指向类范围内的类型来实现异构多态数据结构。
  • 使用标记类型和类型扩展,而不是变体记录(与枚举类型和 case 语句结合使用)。

一个类型为Employee_List的数组可以包含指向兼职和全职员工的指针(将来可能还会包含其他类型的员工)。

-----------------------------------------------------------------------------------
package Personnel is
   type Employee  is tagged limited private;
   type Reference is access all Employee'Class;
   ...
private
   ...
end Personnel;
-----------------------------------------------------------------------------------
with Personnel; 
package Part_Time_Staff is
   type Part_Time_Employee is new Personnel.Employee with 
      record
         ...
      end record;
   ...
end Part_Time_Staff;
-----------------------------------------------------------------------------------
with Personnel;
package Full_Time_Staff is
   type Full_Time_Employee is new Personnel.Employee with
      record
         ...
      end record;
   ...
end Full_Time_Staff;
-----------------------------------------------------------------------------------

...

type Employee_List is array (Positive range <>) of Personnel.Reference;

Current_Employees : Employee_List (1..10);

...

Current_Employees(1) := new Full_Time_Staff.Full_Time_Employee;
Current_Employees(2) := new Part_Time_Staff.Part_Time_Employee;
...

多态性是一种将一组抽象之间的差异提取出来的方法,以便程序可以根据共同的属性进行编写。多态性允许异构数据结构中不同的对象以相同的方式进行处理,基于对根标记类型上定义的调度操作。这样就无需使用case语句来选择每个特定类型所需的处理。指南 5.6.3 讨论了使用case语句带来的维护影响。

枚举类型、变体记录和 case 语句难以维护,因为对数据类型特定变体的专业知识往往分散在整个程序中。当您创建标记类型层次结构(标记类型和类型扩展)时,可以避免变体记录、case 语句和仅支持变体记录判别的单个枚举类型。此外,通过将与单个操作相关的所有原语都调用共同的“操作特定”代码,您可以将有关变体的“专业知识”定位在数据结构内。

有关标记类型的更详细讨论,请参见指南 9.2.1。


例外情况

[编辑 | 编辑源代码]

在某些情况下,您可能希望使用变体记录方法来围绕操作组织模块化。例如,对于图形输出,您可能会发现使用变体记录更易于维护。您必须权衡添加新操作是否比添加新变体工作量更小。


嵌套记录

[编辑 | 编辑源代码]
  • 记录结构不应总是扁平的。提取出公共部分。
  • 对于大型记录结构,将相关组件分组到较小的子记录中。
  • 对于嵌套记录,选择在引用内部元素时可读性良好的元素名称。
  • 考虑使用类型扩展来组织大型数据结构。
type Coordinate is
   record
      Row    : Local_Float;
      Column : Local_Float;
   end record;
type Window is
   record
      Top_Left     : Coordinate;
      Bottom_Right : Coordinate;
   end record;


您可以通过将复杂数据结构组合成熟悉的构建块来使其易于理解和理解。这种技术对于具有自然分组部分的大型记录类型尤其有效。基于共同质量或目的而分解到单独声明的记录中的组件,对应于比大型记录所代表的更低级别的抽象。

在设计复杂数据结构时,您必须考虑类型组合或类型扩展是否是最合适的技术。类型组合指的是创建类型本身为记录的记录组件。您通常需要这两种技术的混合,即,通过类型组合包含一些组件,通过类型扩展创建其他组件。如果“中间”记录都是同一抽象家族的实例,则类型扩展可能会提供更简洁的设计。另请参见指南 5.4.2 和 9.2.1。


命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[编辑 | 编辑源代码]

仔细选择的较大记录组件的名称(用于选择较小的记录),可以提高可读性,例如

if Window1.Bottom_Right.Row > Window2.Top_Left.Row then . . .

动态数据

[编辑 | 编辑源代码]
  • 区分静态数据和动态数据。谨慎使用动态分配的对象。
  • 仅当需要动态创建和销毁动态分配的数据结构,或需要通过不同的名称引用它们时,才使用动态分配的数据结构。
  • 不要丢弃指向未分配对象的指针。
  • 不要留下指向已分配对象的悬空引用。
  • 初始化记录中所有访问变量和组件。
  • 不要依赖内存释放。
  • 显式释放内存。
  • 使用长度子句来指定总分配大小。
  • Storage_Error.
  • 提供处理程序。
  • 使用受控类型来实现操作动态数据的私有类型。
  • 除非您的运行时环境可靠地回收动态堆存储,否则避免使用无约束记录对象。
    • 除非您的运行时环境可靠地回收动态堆存储,否则仅在库包、主子程序或永久任务的最外层、未嵌套的声明部分声明以下项目
    • 访问类型
    • 具有非静态边界的约束复合对象
    • 除无约束记录之外的其他无约束复合类型对象
  • 复合对象足够大(在编译时),以便编译器在堆上隐式分配
    • 除非您的运行时环境可靠地回收动态堆存储,或者您正在创建永久的、动态分配的任务,否则请避免在以下情况下声明任务
    • 组件为任务的无约束数组子类型
    • 包含任务数组的组件的判别记录子类型,其中数组大小取决于判别的值
    • 除库包或主子程序的最外层、未嵌套的声明部分之外的任何声明区域

示例

未静态约束的任务数组

[编辑 | 编辑源代码]

P1 := new Object;
P2 := P1;
Unchecked_Object_Deallocation(P2);

这些行展示了如何创建悬空引用。

X := P1.all;

由于引用了已分配对象,因此此行可能会引发异常。在以下三行中,如果P1

P1 := new Object;
...
P1 := P2;

的值没有分配给任何其他指针,则第一行创建的对象在第三行之后将不再可访问。指向已分配对象的唯一指针已被丢弃。以下代码展示了使用Finalize

with Ada.Finalization;
package List is
   type Object is private;
   function "=" (Left, Right : Object) return Boolean;  -- element-by-element comparison
   ... -- Operations go here
private
   type Handle is access List.Object;
   type Object is new Ada.Finalization.Controlled with
      record
         Next : List.Handle;
         ... -- Useful information go here
      end record;
   procedure Adjust (L : in out List.Object);
   procedure Finalize (L : in out List.Object);
end List;
package body List is
   Free_List : List.Handle;
   ...
   procedure Adjust (L : in out List.Object) is
   begin
      L := Deep_Copy (L);
   end Adjust;
   procedure Finalize (L : in out List.Object) is
   begin
      -- Chain L to Free_List
   end Finalize;
end List;

原理

来确保在对象被终结(即超出范围)时,动态分配的元素被链接到一个空闲列表上。

[编辑 | 编辑源代码]另请参见 6.3.2 中有关这些问题的变体。动态分配的对象是由分配器执行创建的对象(new

)。通过访问变量引用的已分配对象允许您生成别名,即对同一对象的多个引用。当您通过另一个名称引用已分配的对象时,可能会出现异常行为。这被称为悬空引用。完全将仍然有效的对象与所有名称分离被称为丢弃指针。没有与名称关联的动态分配对象无法被引用或显式释放。

丢弃指针依赖于隐式内存管理器来回收空间。它还引发了读者对失去对对象的访问是故意的还是意外的疑问。Ada 环境不需要提供动态分配对象的释放。如果提供,它可能被隐式提供(当它们的访问类型超出范围时,对象会被释放),显式提供(当Ada.Unchecked_DeallocationAda 环境不需要提供动态分配对象的释放。如果提供,它可能被隐式提供(当它们的访问类型超出范围时,对象会被释放),显式提供(当被调用时,对象会被释放),或两者兼而有之。为了提高存储空间被回收的可能性,最好在您完成使用每个动态创建的对象时显式调用Ada 环境不需要提供动态分配对象的释放。如果提供,它可能被隐式提供(当它们的访问类型超出范围时,对象会被释放),显式提供(当。对

的调用还记录了要放弃对象的故意决定,从而使代码更易于阅读和理解。为了绝对确保空间被回收并重新使用,请管理您自己的“空闲列表”。跟踪您已完成使用哪些对象,并在以后重新使用它们,而不是动态分配新对象。

悬空引用的危险在于您可能会尝试使用它们,从而访问已释放给内存管理器的内存,该内存可能随后已分配给程序中其他部分的另一个用途。当您从这样的内存读取数据时,可能会发生意外错误,因为程序的其他部分可能之前已在其中写入完全无关的数据。更糟糕的是,当您写入这样的内存时,您可以通过更改该代码动态分配的变量的值来导致代码中看似无关部分的错误。这种类型的错误可能非常难以找到。最后,这些错误可能在您没有编写的环境部分中触发,例如,在内存管理系统本身中,它可能动态分配内存以记录有关您的动态分配内存的信息。请记住,记录或数组的任何未重置组件也可能是悬空引用,或者可能承载表示不一致数据的位模式。访问类型的组件始终默认初始化为null

无论何时使用动态分配,都有可能耗尽空间。Ada 提供了一种机制(长度子句)用于在编译时请求分配空间池的大小。但请预期,在运行时仍有可能耗尽空间。为异常准备处理程序Storage_Error,并仔细考虑在每种情况下程序中可以包含哪些替代方案。

有一种观点认为应该避免所有动态分配。这种观点主要基于对执行过程中内存耗尽的恐惧。诸如长度子句和异常处理程序之类的机制为Storage_Error提供了对内存分区和错误恢复的显式控制,使得这种恐惧毫无根据。

在实现复杂的数据结构(树、列表、稀疏矩阵等)时,通常会使用访问类型。如果不注意,可能会用这些动态分配的对象耗尽所有存储空间。可以导出一个释放操作,但无法确保它会在适当的地方被调用;实际上,你是在信任客户端。如果从受控类型派生(有关更多信息,请参见 8.3.3 和 9.2.3),可以使用终结来处理动态数据的释放,从而避免存储耗尽。用户定义的存储池可以更好地控制分配策略。

一个相关但不同的问题是共享语义与复制语义:即使数据结构是使用访问类型实现的,也不一定希望共享语义。在某些情况下,你真正想要的是 :=创建一个副本,而不是一个新的引用,并且你真正想要的是=比较内容,而不是引用。应该将结构实现为受控类型。如果需要复制语义,可以重新定义调整以执行深层复制,以及=以执行对内容的比较。还可以重新定义以下代码展示了使用以确保当对象被终结(即超出范围)时,动态分配的元素被链接到一个空闲列表(或者通过Ada 环境不需要提供动态分配对象的释放。如果提供,它可能被隐式提供(当它们的访问类型超出范围时,对象会被释放),显式提供(当).

Ada 程序在执行过程中对动态(堆)存储的隐式使用会带来重大风险,可能会导致软件故障。Ada 运行时环境可能会在与复合对象、动态创建的任务和连接相关的操作中使用隐式动态(堆)存储。通常,用于管理动态分配和回收堆存储的算法会导致碎片或泄漏,这会导致存储耗尽。通常很难或不可能从存储耗尽或Storage_Error中恢复,除非重新加载并重新启动 Ada 程序。避免所有隐式分配的使用将非常严格。另一方面,阻止显式和隐式释放可以显著降低碎片和泄漏的风险,而不会过度限制对复合对象、访问值、任务对象和连接的使用。


例外情况

[edit | edit source]

如果复合对象足够大,可以分配到堆中,仍然可以将其声明为inin out形式参数。该指南旨在避免在对象声明、形式out参数或函数返回的值中声明对象。

应该监控堆中的泄漏和/或碎片。如果它们达到稳态,并且在程序或分区执行期间没有持续增加,可以使用指南中描述的结构。


别名对象

[edit | edit source]

指南

[edit | edit source]
  • 尽量减少对别名变量的使用。
  • 对静态创建的、不规则数组使用别名(理由 1995,§3.7.1)。
  • 当想要隐藏内部连接和簿记信息时,使用别名来引用数据结构的一部分。

示例

[edit | edit source]
package Message_Services is
   type Message_Code_Type is range 0 .. 100;
   subtype Message is String;
   function Get_Message (Message_Code: Message_Code_Type)
     return Message;
   pragma Inline (Get_Message);
end Message_Services;
package body Message_Services is
   type Message_Handle is access constant Message;
   Message_0 : aliased constant Message := "OK";
   Message_1 : aliased constant Message := "Up";
   Message_2 : aliased constant Message := "Shutdown";
   Message_3 : aliased constant Message := "Shutup";
   . . .
   type Message_Table_Type is array (Message_Code_Type) of Message_Handle;
   
   Message_Table : Message_Table_Type :=
     (0 => Message_0'Access,
      1 => Message_1'Access,
      2 => Message_2'Access,
      3 => Message_3'Access,
      -- etc.
     );
   function Get_Message (Message_Code : Message_Code_Type)
     return Message is
   begin
      return Message_Table (Message_Code).all;
   end Get_Message;
end Message_Services;

以下代码片段展示了别名对象的使用,使用属性'Access来实现一个管理对象散列表的通用组件

generic
   type Hash_Index is mod <>;
   type Object is tagged private;
   type Handle is access all Object;
   with function Hash (The_Object : in Object) return Hash_Index;
package Collection is
   function Insert (Object : in Collection.Object) return Collection.Handle;
   function Find (Object : in Collection.Object) return Collection.Handle;
   Object_Not_Found : exception;

   ...
private
   type Cell;
   type Access_Cell is access Cell;
end Collection;
package body Collection is
   type Cell is
   record
      Value : aliased Collection.Object;
      Link  : Access_Cell;
   end record;
   type Table_Type is array (Hash_Index) of Access_Cell;

   Table : Table_Type;
   -- Go through the collision chain and return an access to the useful data.
   function Find (Object : in Collection.Object;
                  Index  : in Hash_Index) return Handle is
      Current : Access_Cell := Table (Index);
   begin
      while Current /= null loop
         if Current.Value = Object then
            return Current.Value'Access;
         else
            Current := Current.Link;
         end if;
      end loop;
      raise Object_Not_Found;
   end Find;
   -- The exported one
   function Find (Object : in Collection.Object) return Collection.Handle is
      Index : constant Hash_Index := Hash (Object);
   begin
      return Find (Object, Index);
   end Find;
   ...
end Collection;

原理

[edit | edit source]

别名允许程序员通过间接方式访问声明的对象。由于可以通过多个路径更新别名对象,因此必须谨慎操作以避免意外更新。当将别名对象限制为常量时,可以避免对象被意外修改。在上面的示例中,单个消息对象是被别名的常量消息字符串,因此它们的值不能更改。然后,不规则数组被初始化为对每个常量字符串的引用。

别名允许你通过间接方式操作对象,同时避免动态分配。例如,可以将对象插入到链接列表中,而无需动态分配该对象的空间(理由 1995,§3.7.1)。

别名的另一种用途是在链接数据结构中,试图隐藏封闭容器。这本质上是自引用数据结构的逆运算(参见指南 5.4.7)。如果一个包使用链接数据结构管理一些数据,你可能只想导出表示“有用”数据的访问值。可以使用指向对象的访问来返回指向有用数据的访问,排除用于链接对象的指针。


访问辨别式

[edit | edit source]

指南

[edit | edit source]
  • 使用访问辨别式创建自引用数据结构,即数据结构的其中一个组件指向封闭结构。

示例

[edit | edit source]

参见指南 8.3.6(使用访问辨别式构建迭代器)和 9.5.1(在多重继承中使用访问辨别式)中的示例。


原理

[edit | edit source]

访问辨别式本质上是一个匿名类型的指针,被用作辨别式。由于访问辨别式是匿名访问类型,因此无法声明该类型的其他对象。因此,一旦初始化了辨别式,就会在辨别式及其访问的对象之间创建一个“永久”(对于对象的生命周期)关联。当创建自引用结构时,即结构的某个组件被初始化为指向封闭对象,访问辨别式的“常量”行为会提供正确的行为,以帮助维护结构的完整性。

另见理由(1995,§4.6.3)中有关使用访问辨别式实现对象的多个视图的讨论。

另见指南 6.1.3 中关于任务类型的访问辨别式的示例。


模块类型

[edit | edit source]

指南

[edit | edit source]
  • 当创建需要按位操作的数据结构时,使用模块类型而不是布尔数组,例如.

示例

[edit | edit source]
with Interfaces;
procedure Main is
   type Unsigned_Byte is mod 255;

   X : Unsigned_Byte;
   Y : Unsigned_Byte;
   Z : Unsigned_Byte;
   X1 : Interfaces.Unsigned_16;
begin -- Main
   Z := X or Y;  -- does not cause overflow

   -- Show example of left shift
   X1 := 16#FFFF#;
   for Counter in 1 .. 16 loop
      X1 := Interfaces.Shift_Left (Value => X1, Amount => 1);
   end loop;
end Main;

原理

[edit | edit source]

当比特数已知少于一个字中的比特数和/或性能是一个严重问题时,首选模块类型。当比特数事先未知且性能不是一个严重问题时,布尔数组是合适的。另见指南 10.6.3。


表达式

[edit | edit source]

正确编码的表达式可以增强程序的可读性和可理解性。编码不当的表达式会将程序变成维护者的噩梦。


范围值

[edit | edit source]

指南

[edit | edit source]
  • 使用'第一个'最后一个而不是使用数字字面量来表示范围的第一个或最后一个值。
  • 使用'范围或范围的子类型名称,而不是'第一个 .. '最后一个.
type Temperature      is range All_Time_Low .. All_Time_High;
type Weather_Stations is range            1 ..  Max_Stations;
Current_Temperature : Temperature := 60;
Offset              : Temperature;
...
for I in Weather_Stations loop
   Offset := Current_Temperature - Temperature'First;
   ...
end loop;


在上面的例子中,最好使用Weather_Stationsfor循环中,而不是使用Weather_Stations'First .. Weather_Stations'Last1 .. Max_Stations因为它更清晰、不易出错,并且对类型的定义依赖性更小Weather_Stations。类似地,在偏移量计算中,最好使用Temperature'First而不是使用All_Time_Low因为即使子类型的定义Temperature发生变化,代码仍然是正确的。这增强了程序的可靠性。


命名参数关联是否提高可读性的判断是主观的。当然,简单的或熟悉的子程序,例如交换例程或正弦函数,不需要在过程调用中使用命名关联的额外说明。

[编辑 | 编辑源代码]

当您以这种方式隐式指定范围和属性时,请注意使用正确的子类型名称。很容易在没有意识到的情况下引用一个非常大的范围。例如,给出以下声明

type    Large_Range is new Integer;
subtype Small_Range is Large_Range range 1 .. 10;

type Large_Array is array (Large_Range) of Integer;
type Small_Array is array (Small_Range) of Integer;

则下面的第一个声明工作正常,但第二个声明可能是一个意外,并在大多数机器上引发异常,因为它请求一个巨大的数组(从最小的整数索引到最大的整数)

Array_1 : Small_Array;
Array_2 : Large_Array;

数组属性

[编辑 | 编辑源代码]
  • 使用数组属性'第一个, '最后一个,或'长度而不是使用数字字面量来访问数组。
  • 使用'范围数组的,而不是索引子类型的名称来表达范围。
  • 使用'范围而不是'第一个 .. '最后一个来表达范围。
subtype Name_String is String (1 .. Name_Length);
File_Path : Name_String := (others => ' ');
...
for I in File_Path'Range loop
   ...
end loop;


在上面的例子中,最好使用Name_String'Rangefor循环中,而不是使用Name_String_Size, Name_String'First .. Name_String'Last,或1 .. 30因为它更清晰、不易出错,并且对Name_StringName_String_Size的定义依赖性更小。如果Name_String被更改为具有不同的索引类型,或者如果数组的边界被更改,这仍然可以正常工作。这增强了程序的可靠性。


圆括号表达式

[编辑 | 编辑源代码]
  • 使用圆括号来指定子表达式求值的顺序以澄清表达式(NASA 1987)。
  • 使用圆括号来指定子表达式的求值顺序,其正确性取决于从左到右的求值。
(1.5 * X**2)/A - (6.5*X + 47.0)
2*I + 4*Y + 8*Z + C


Ada 的运算符优先级规则在 Ada 参考手册 1995,第 4.5 节 [带注释的] 中定义,并遵循相同的普遍接受的代数运算符优先级。Ada 中的强类型机制与常见的优先级规则相结合,使许多圆括号变得不必要。但是,当出现不常见的运算符组合时,即使优先级规则适用,添加圆括号也可能会有所帮助。表达式

5 + ((Y ** 3) mod 10)

更清晰,等同于

5 + Y**3 mod 10

求值规则确实指定了对具有相同优先级级别的运算符从左到右进行求值。但是,在检查表达式的正确性时,它是最常被忽视的求值规则。


逻辑的肯定形式

[编辑 | 编辑源代码]
  • 避免依赖于负值使用的名称和结构。
  • 选择标志的名称,使它们表示可以以肯定形式使用的状态。

使用

if Operator_Missing then

而不是两者

if not Operator_Found then

if not Operator_Missing then


当以肯定形式陈述时,关系表达式可能更易读且更易理解。作为选择名称的辅助方法,请考虑在条件构造中,最常用的分支应首先遇到。


例外情况

[编辑 | 编辑源代码]

在某些情况下,负形式是不可避免的。如果关系表达式更好地反映了代码中的实际情况,那么不建议反转测试以遵守此指南。


逻辑运算符的短路形式

[编辑 | 编辑源代码]
  • 使用逻辑运算符的短路形式来指定条件的顺序,当一个条件失败意味着另一个条件将引发异常时。

使用

if Y /= 0 or else (X/Y) /= 10 then

if Y /= 0 then
   if (X/Y) /= 10 then

而不是两者

if Y /= 0 and (X/Y) /= 10 then

if (X/Y) /= 10 then

以避免Constraint_Error。

使用

if Target /= null and then Target.Distance < Threshold then

而不是

if Target.Distance < Threshold then

以避免引用不存在的对象中的字段。


使用短路控制形式可以防止一类数据相关的错误或异常,这些错误或异常可能是表达式求值的结果。短路形式保证了求值的顺序,并保证了exit从关系表达式序列中退出,只要可以确定表达式的结果。

在没有短路形式的情况下,Ada 不保证表达式求值的顺序,也不保证在关系表达式求值为False(对于)或True(对于).


命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[编辑 | 编辑源代码]

如果某个表达式的所有部分都必须始终被计算,那么该表达式可能违反了指南 4.1.4,该指南限制了函数中的副作用。


使用实数操作数进行运算的精度

[编辑 | 编辑源代码]
  • 使用<=>=在包含实数操作数的关系表达式中,而不是=.
Current_Temperature   : Temperature :=       0.0;
Temperature_Increment : Temperature := 1.0 / 3.0;
Maximum_Temperature   : constant    :=     100.0;
...
loop
   ...
   Current_Temperature :=
         Current_Temperature + Temperature_Increment;
   ...
   exit when Current_Temperature >= Maximum_Temperature;
   ...
end loop;


固定点和浮点数,即使从相似的表达式派生而来,也可能并不完全相等。硬件中实数的不精确、有限表示总是存在舍入误差,因此,两个实数的构建路径或历史记录的任何变化都有可能导致不同的数字,即使这些路径或历史记录在数学上是等价的。

Ada 对模型区间的定义也意味着使用<=比使用<=.


命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[编辑 | 编辑源代码]

指南 7.2.7 中介绍了浮点运算。


例外情况

[编辑 | 编辑源代码]

如果您的应用程序必须测试实数的精确值(例如,测试特定机器上算术的精度),则必须使用=。但是,永远不要在实数操作数上使用=作为退出循环的条件。


即使程序的全局结构组织良好,但语句使用不当或过于复杂,也会使程序难以阅读和维护。您应该努力使语句的使用简单一致,以实现本地程序结构的清晰度。本节中的某些指南建议使用或避免使用特定的语句。正如各个指南中指出的那样,严格遵守这些指南将是过分的,但经验表明,它们通常会导致代码具有更高的可靠性和可维护性。


  • 将嵌套表达式的深度最小化 (Nissen 和 Wallis 1984)。
  • 将嵌套控制结构的深度最小化 (Nissen 和 Wallis 1984)。
  • 尝试使用简化启发式方法(参见以下注释)。

为可选参数提供非默认值时使用命名关联。

[编辑 | 编辑源代码]
  • 不要将表达式或控制结构嵌套到超过五层的嵌套级别。


以下代码部分

if not Condition_1 then
   if Condition_2 then
      Action_A;
   else  -- not Condition_2
      Action_B;
   end if;
else  -- Condition_1
   Action_C;
end if;

可以更清晰地重写,并且嵌套更少

if Condition_1 then
   Action_C;
elsif Condition_2 then
   Action_A;
else  -- not (Condition_1 or Condition_2)
   Action_B;
end if;


深度嵌套的结构令人困惑,难以理解,难以维护。问题在于难以确定程序的哪个部分包含在任何给定级别。对于表达式,这在实现正确的位置的平衡分组符号以及实现所需的运算符优先级方面非常重要。对于控制结构,问题涉及控制的哪个部分。具体来说,某个语句是否处于适当的嵌套级别,也就是说,它是否嵌套得太深或太浅,或者某个语句是否与正确的选择相关联,例如,对于ifcase语句?缩进很有帮助,但它不是万能的。从视觉上检查缩进代码的对齐方式(主要是中间级别)充其量是一个不确定的工作。为了最大限度地降低代码的复杂性,请将最大嵌套级别限制在三到五层之间。


命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[编辑 | 编辑源代码]

问问自己以下问题,以帮助您简化代码

  • 表达式的一部分能否放入常量或变量中?
  • 较低嵌套控制结构的一部分是否代表一个重要的,可能可重复使用的计算,我可以将其分解为子程序?
  • 我可以将这些嵌套的if语句转换为case语句吗?
  • 我是否在使用else if,而我本可以使用elsif?
  • 吗?我可以重新排序控制此嵌套结构的条件表达式吗?
  • 是否有其他更简单的设计?

例外情况

[编辑 | 编辑源代码]

如果深度嵌套经常需要,那么代码的整体设计决策可能应该更改。某些算法需要深度嵌套的循环和由条件分支控制的段。它们可以继续使用,归因于它们的效率、熟悉度和时间证明的效用。当需要嵌套时,请谨慎行事,并特别注意标识符以及循环和块名称的选择。


  • 使用切片而不是循环来复制数组的一部分。
First  : constant Index := Index'First;
Second : constant Index := Index'Succ(First);
Third  : constant Index := Index'Succ(Second);
type Vector is array (Index range <>) of Element;
subtype Column_Vector is Vector (Index);
type    Square_Matrix is array  (Index) of Column_Vector;
subtype Small_Range  is Index range First .. Third;
subtype Diagonals    is Vector (Small_Range);
type    Tri_Diagonal is array  (Index) of Diagonals;
Markov_Probabilities : Square_Matrix;
Diagonal_Data        : Tri_Diagonal;
...
-- Remove diagonal and off diagonal elements.
Diagonal_Data(Index'First)(First) := Null_Value;
Diagonal_Data(Index'First)(Second .. Third) :=
      Markov_Probabilities(Index'First)(First .. Second);
for I in Second .. Index'Pred(Index'Last) loop
   Diagonal_Data(I) :=
         Markov_Probabilities(I)(Index'Pred(I) .. Index'Succ(I));
end loop;
Diagonal_Data(Index'Last)(First .. Second) :=
      Markov_Probabilities(Index'Last)(Index'Pred(Index'Last) .. Index'Last);
Diagonal_Data(Index'Last)(Third) := Null_Value;


使用切片的赋值语句比循环更简单、更清晰,可以帮助读者了解预期的操作。另请参见指南 10.5.7,了解切片赋值与循环的潜在性能问题。


情况语句

[编辑 | 编辑源代码]
  • 尽量减少使用others选择在case语句的循环关联。
  • 中。不要在case语句带来的维护影响。
  • 使用case语句中使用枚举文字的范围,而应使用if/elsif语句(如果可能)。
  • 使用类型扩展和分派,而不是case语句(如果可能)。
type Color is (Red, Green, Blue, Purple);
Car_Color : Color := Red;
...
case Car_Color is
   when Red .. Blue => ...
   when Purple      => ...
end case;  -- Car_Color

现在考虑对类型进行更改

type Color is (Red, Yellow, Green, Blue, Purple);

此更改可能在case语句中产生未被注意到且不受欢迎的影响。如果选择被明确枚举,如下所示when Red | Green | Blue =>而不是when Red .. Blue =>,那么case语句将无法编译。这将迫使维护人员在以下情况下做出关于如何处理的明智决定黄色.

在下面的示例中,假设已发布了一个菜单,并且用户应输入四个选项之一。假设User_Choice声明为字符并且Terminal_IO.Get处理用户输入中的错误。使用if/elsif语句的较不直观的替代方法显示在case语句之后

Do_Menu_Choices_1:
   loop
      ...
  
      case User_Choice is
         when 'A'    => Item := Terminal_IO.Get ("Item to add");
         when 'D'    => Item := Terminal_IO.Get ("Item to delete");
         when 'M'    => Item := Terminal_IO.Get ("Item to modify");
         when 'Q'    => exit Do_Menu_Choices_1;
  
         when others => -- error has already been signaled to user
            null;
      end case;
   end loop Do_Menu_Choices_1;

Do_Menu_Choices_2:
   loop
      ...

      if User_Choice = 'A' then
         Item := Terminal_IO.Get ("Item to add");

      elsif User_Choice = 'D' then
         Item := Terminal_IO.Get ("Item to delete");

      elsif User_Choice = 'M' then
         Item := Terminal_IO.Get ("Item to modify");

      elsif User_Choice = 'Q' then
         exit Do_Menu_Choices_2;

      end if;
   end loop Do_Menu_Choices_2;


原理

[edit | edit source]

应了解对象的全部可能值,并为每个值分配特定的操作。使用others子句可能会阻止开发人员仔细考虑每个值的相应操作。如果未使用others子句,编译器会警告用户遗漏的值。

如果others表达式子类型具有许多值,例如,则可能无法避免在case语句中使用通用整数, 宽字符,或字符)。如果选择的值范围比子类型的范围小,则应考虑使用if/elsif语句。请注意,您必须提供others备选方案,当您的case表达式是泛型类型时。

应明确枚举每个可能的值。范围可能很危险,因为范围可能会发生变化,并且case语句可能不会重新检查。如果您已声明子类型以对应于感兴趣的范围,则可以考虑使用此命名子类型。

在许多情况下,case语句可提高代码的可读性。有关性能注意事项的讨论,请参见指南 10.5.3。在许多实现中,case语句可能更有效。

当您向数据结构添加新的变体时,类型扩展和分派会减轻维护负担。另请参见指南 5.4.2 和 5.4.4。


命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[edit | edit source]

需要在case语句中使用的范围可以使用受约束的子类型来增强可维护性。它更容易维护,因为范围的声明可以放置在逻辑上属于抽象的一部分的地方,而不是隐藏在可执行代码的case语句中

subtype Lower_Case is Character range 'a' .. 'z';
subtype Upper_Case is Character range 'A' .. 'Z';
subtype Control    is Character range Ada.Characters.Latin_1.NUL ..
                                      Ada.Characters.Latin_1.US;
subtype Numbers    is Character range '0' .. '9';
...
case Input_Char is
   when Lower_Case => Capitalize(Input_Char);
   when Upper_Case => null;
   when Control    => raise Invalid_Input;
   when Numbers    => null;
   ...
end case;


例外情况

[edit | edit source]

仅当用户确信永远不会在旧值之间插入新值时,才可以使用范围作为可能值,例如在 ASCII 字符范围中'a' .. 'z'.


循环

[edit | edit source]

指南

[edit | edit source]
  • 使用for循环,尽可能地使用。
  • 使用while循环,当无法在进入循环之前计算迭代次数,但可以在循环顶部应用简单的延续条件时。
  • 使用带有exit语句的普通循环来处理更复杂的情况。
  • 避免在exit循环中使用whilefor语句。
  • 最大程度地减少exit循环的方法。

示例

[edit | edit source]

要遍历数组的所有元素

for I in Array_Name'Range loop
   ...
end loop;

要遍历链表中的所有元素

Pointer := Head_Of_List;
while Pointer /= null loop
   ...
   Pointer := Pointer.Next;
end loop;

经常会出现需要“循环和一半”的情况。为此,请使用

P_And_Q_Processing:
   loop
      P;
      exit P_And_Q_Processing when Condition_Dependent_On_P;
      Q;
   end loop P_And_Q_Processing;

而不是

P;
while not Condition_Dependent_On_P loop
   Q;
   P;
end loop;

原理

[edit | edit source]

一个for循环是有界的,因此不能是“无限循环”。Ada 语言强制执行此操作,它要求在循环规范中使用有限范围,并且不允许修改for循环的循环计数器,该循环计数器由循环内执行的语句修改。这为读者和作者提供了与其他形式循环无关的确定性理解。一个for循环也更容易维护,因为迭代范围可以使用循环操作的数据结构的属性来表示,如上面的示例所示,其中每次修改数组声明时,范围都会自动更改。出于这些原因,最好尽可能地使用for循环,也就是说,只要可以使用简单表达式来描述循环计数器的第一个值和最后一个值,就可以使用它。

while循环已成为大多数程序员非常熟悉的结构。一目了然,它指示循环继续的条件。如果无法使用while循环,但存在描述循环应继续的条件的简单布尔表达式时,请使用for循环,如上面的示例所示。

应在更复杂的情况下使用普通循环语句,即使可以使用forwhile循环结合额外的标志变量或exit语句来设计解决方案。选择循环结构的标准是尽可能清晰且易于维护。使用exit语句从forwhile循环中退出不是一个好主意,因为在显然已在循环顶部描述了完整的循环条件集之后,它会误导读者。遇到普通循环语句的读者希望看到exit语句带来的维护影响。

有一些熟悉的循环情况最适合使用普通循环语句来实现。例如,Pascal 的repeat until循环的语义,其中循环在终止测试发生之前始终至少执行一次,最适合使用带有一个exit语句的普通循环来实现,该语句位于循环末尾。另一种常见情况是“循环和一半”结构,如上面的示例所示,其中循环必须在语句体序列中的某个位置终止。使用while循环模拟的复杂的“循环和一半”结构通常需要引入标志变量或在循环之前和期间复制代码,如示例所示。这种扭曲使代码更加复杂,可靠性降低。

最大程度地减少exit循环,以使循环更易于读者理解。您需要从循环中退出超过两种方式的情况应该很少见。如果需要,请务必对所有情况使用exit语句,而不是向exit循环添加forwhile语句。


当存在嵌套块结构时,可能难以确定哪个

[edit | edit source]

指南

[edit | edit source]
  • 使用exit语句,以增强循环终止代码的可读性 (NASA 1987)。
  • 使用exit when ...而不是if ... then exit尽可能地使用 (NASA 1987)。
  • 复查exit语句放置。

示例

[edit | edit source]

参见指南 5.1.1 和指南 5.6.4 中的示例。

原理

[edit | edit source]

使用exit语句比尝试向while循环条件添加布尔标志来模拟从循环中间退出更易读。即使所有exit语句都集中在循环体顶部,将复杂条件分解为多个exit语句可以简化代码,并使其更易读、更清晰。两个exit语句的顺序执行通常比短路控制形式更清晰。

exit when形式优于if ... then exit形式,因为它通过不将其嵌套在任何控制结构中,使单词exit更醒目。仅当除了if ... then exit语句之外,还必须有条件地执行其他语句时,才需要exit形式。例如

Process_Requests:
   loop
      if Status = Done then

         Shut_Down;
         exit Process_Requests;

      end if;

      ...

   end loop Process_Requests;

具有许多分散的exit语句的循环可能表明对算法中循环目的的模糊思考。这种算法可能可以通过其他方式更好地编写代码,例如使用一系列循环。一些返工通常可以减少exit语句的数量,并使代码更清晰。

另请参见指南 5.1.3 和 5.6.4。


递归和迭代边界

[edit | edit source]

指南

[edit | edit source]
  • 考虑为循环指定边界。
  • 考虑为递归指定边界。

示例

[edit | edit source]

建立迭代边界

Safety_Counter := 0;
Process_List:
   loop
      exit when Current_Item = null;
      ...
      Current_Item := Current_Item.Next;
      ...
      Safety_Counter := Safety_Counter + 1;
      if Safety_Counter > 1_000_000 then
         raise Safety_Error;
      end if;
   end loop Process_List;

建立递归边界

subtype Recursion_Bound is Natural range 0 .. 1_000;

procedure Depth_First (Root           : in     Tree;
                       Safety_Counter : in     Recursion_Bound
                                      := Recursion_Bound'Last) is
begin
   if Root /= null then
      if Safety_Counter = 0 then
         raise Recursion_Error;
      end if;
      Depth_First (Root           => Root.Left_Branch,
                   Safety_Counter => Safety_Counter - 1);

      Depth_First (Root           => Root.Right_Branch,
                   Safety_Counter => Safety_Counter - 1);
      ... -- normal subprogram body
   end if;
end Depth_First;

以下是此子程序使用示例。 一个调用指定了最大递归深度为 50。第二个使用默认值 (1,000)。第三个使用计算的边界

Depth_First(Root => Tree_1, Safety_Counter => 50);
Depth_First(Tree_2);
Depth_First(Root => Tree_3, Safety_Counter => Current_Tree_Height);

原理

[edit | edit source]

递归和使用除for语句以外的结构的迭代可能是无限的,因为预期终止条件没有出现。 这种故障有时非常微妙,可能很少出现,并且可能难以检测,因为外部表现可能不存在或延迟相当长的时间。

通过除了循环本身之外还包括计数器和对计数器值的检查,可以防止许多形式的无限循环。 包括此类检查是安全编程技术 (Anderson 和 Witty 1978) 的一个方面。

这些检查的边界不必准确,只要符合实际情况即可。 这种计数器和检查不是程序主要控制结构的一部分,而是作为运行时“安全网”的良性添加,允许错误检测,并可能从潜在的无限循环或无限递归中恢复。

命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[edit | edit source]

如果循环使用for迭代方案 (指南 5.6.4),则它遵循此指南。

例外情况

[edit | edit source]

嵌入式控制应用程序具有旨在无限循环的循环。 这些应用程序中只有少数循环应该作为此指南的例外。 这些例外应该是经过深思熟虑的 (并经过记录的) 策略决定。

此指南对于安全关键系统至关重要。 对于其他系统,它可能过于繁琐。


Goto 语句

[edit | edit source]

指南

[edit | edit source]

不要使用goto语句带来的维护影响。

原理

[edit | edit source]

一个goto语句是非结构化的控制流更改。 更糟糕的是,该标签不需要指示相应goto语句的位置。 这使得代码难以阅读,并使其正确执行存在疑问。

其他语言使用goto语句来实现循环退出和异常处理。 Ada 对这些构造的支持使得goto语句极其罕见。

命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[edit | edit source]

如果您必须使用goto语句,则使用空白将其和标签突出显示。 在标签处指示相应goto语句的位置。


Return 语句

[edit | edit source]

指南

[edit | edit source]
  • 最小化return语句 从子程序 (NASA 1987) 中退出。
  • 突出显示return语句使用注释或空白,以防止它们在其他代码中丢失。

示例

[edit | edit source]

以下代码片段比必要更长更复杂

if Pointer /= null then
   if Pointer.Count > 0 then
      return True;
   else  -- Pointer.Count = 0
      return False;
   end if;
else  -- Pointer = null
   return False;
end if;

它应该用更短、更简洁和更清晰的等效行替换

return Pointer /= null and then Pointer.Count > 0;

原理

[edit | edit source]

过度使用 return 会使代码混乱且难以阅读。 仅在必要时使用return语句。 子程序中过多的 return 可能表明逻辑混乱。 如果应用程序需要多个 return,则在同一级别使用它们 (即,像在case语句的不同分支中一样),而不是分散在整个子程序代码中。 一些修改通常可以将return语句的数量减少到一个,并使代码更清晰。

例外情况

[edit | edit source]

如果这样做会影响自然结构和代码可读性,则不要避免使用return语句。


指南

[edit | edit source]
  • 使用块来局部化声明的范围。
  • 使用块来执行局部重命名。
  • 使用块来定义局部异常处理程序。

示例

[edit | edit source]
with Motion;
with Accelerometer_Device;
...

   ---------------------------------------------------------------------
   function Maximum_Velocity return Motion.Velocity is

      Cumulative : Motion.Velocity := 0.0;

   begin  -- Maximum_Velocity

      -- Initialize the needed devices
      ...

      Calculate_Velocity_From_Sample_Data:
         declare
            use type Motion.Acceleration;

            Current       : Motion.Acceleration := 0.0;
            Time_Delta    : Duration;

         begin  -- Calculate_Velocity_From_Sample_Data
            for I in 1 .. Accelerometer_Device.Sample_Limit loop

               Get_Samples_And_Ignore_Invalid_Data:
                  begin
                     Accelerometer_Device.Get_Value(Current, Time_Delta);
                  exception
                     when Constraint_Error =>
                        null; -- Continue trying

                     when Accelerometer_Device.Failure =>
                        raise Accelerometer_Device_Failed;
                  end Get_Samples_And_Ignore_Invalid_Data;

               exit when Current <= 0.0; -- Slowing down

               Update_Velocity:
                  declare
                     use type Motion.Velocity;
                     use type Motion.Acceleration;

                  begin
                     Cumulative := Cumulative + Current * Time_Delta;

                  exception
                     when Constraint_Error =>
                        raise Maximum_Velocity_Exceeded;
                  end Update_Velocity;

            end loop;
         end Calculate_Velocity_From_Sample_Data;

      return Cumulative;

   end Maximum_Velocity;
   ---------------------------------------------------------------------
...

原理

[edit | edit source]

块可以分解大型代码段,并隔离与每个代码子部分相关的细节。 当声明性块描述该代码时,仅在特定代码部分中使用的变量将清晰可见。

重命名可以简化算法的表达,并提高对给定代码部分的可读性。 但当重命名子句在视觉上与它适用的代码分离时,就会令人困惑。 声明性区域允许重命名在读者检查使用该缩写的代码时立即可见。 指南 5.7.1 讨论了关于use子句的类似指南。

局部异常处理程序可以在靠近起源点的地方捕获异常,并允许对它们进行处理、传播或转换。


  • 使用聚合而不是一系列赋值来为记录的所有组件赋值。
  • 在构建要作为实际参数传递的记录时,使用聚合而不是临时变量。
  • 仅在参数存在常规排序时使用位置关联。

最好使用聚合

Set_Position((X, Y));
Employee_Record := (Number     => 42,
                    Age        => 51,
                    Department => Software_Engineering);

而不是使用连续赋值或临时变量

Temporary_Position.X := 100;
Temporary_Position.Y := 200;
Set_Position(Temporary_Position);
Employee_Record.Number     := 42;
Employee_Record.Age        := 51;
Employee_Record.Department := Software_Engineering;

在维护期间使用聚合是有益的。如果记录结构被更改,但相应的聚合未被更改,编译器会标记聚合赋值中缺少的字段。它将无法检测到应该向赋值语句列表添加新的赋值语句这一事实。

聚合也可以真正方便地将数据项组合成作为参数传递信息的记录或数组结构。命名组件关联使聚合更具可读性。

有关聚合的性能影响,请参见指南 10.4.5。



可见性

[编辑 | 编辑源代码]

如指南 4.2 所述,Ada 通过其可见性控制功能强制实施信息隐藏和关注点分离的能力是该语言最重要的优势之一。破坏这些功能,例如,通过过于自由地使用use子句,是浪费和危险的。


Use 子句

[编辑 | 编辑源代码]
  • 当您需要为运算符提供可见性时,请使用use type子句的类似指南。
  • 避免/最小化使用use子句 (Nissen 和 Wallis 1984)。
  • 考虑使用包重命名子句而不是use子句用于包。
  • 考虑在以下情况下使用use子句
    • 当需要标准包且没有引入歧义引用时
    • 当需要对枚举文字的引用时
  • 本地化所有use子句的效果。

这是对指南 4.2.3 中示例的修改。的影响use子句是本地化的

----------------------------------------------------------------------------------
package Rational_Numbers is
   type Rational is private;
   function "=" (X, Y : Rational) return Boolean;
   function "/" (X, Y : Integer)  return Rational;  -- construct a rational number
   function "+" (X, Y : Rational) return Rational;
   function "-" (X, Y : Rational) return Rational;
   function "*" (X, Y : Rational) return Rational;
   function "/" (X, Y : Rational) return Rational;  -- rational division
private
   ...
end Rational_Numbers;
----------------------------------------------------------------------------------
package body Rational_Numbers is
   procedure Reduce (R : in out Rational) is . . . end Reduce;
   . . .
end Rational_Numbers;
----------------------------------------------------------------------------------
package Rational_Numbers.IO is
   ...

   procedure Put (R : in  Rational);
   procedure Get (R : out Rational);
end Rational_Numbers.IO;
----------------------------------------------------------------------------------
with Rational_Numbers;
with Rational_Numbers.IO;
with Ada.Text_IO;
procedure Demo_Rationals is
   package R_IO renames Rational_Numbers.IO;

   use type Rational_Numbers.Rational;
   use R_IO;
   use Ada.Text_IO;

   X : Rational_Numbers.Rational;
   Y : Rational_Numbers.Rational;
begin  -- Demo_Rationals
   Put ("Please input two rational numbers: ");
   Get (X);
   Skip_Line;
   Get (Y);
   Skip_Line;
   Put ("X / Y = ");
   Put (X / Y);
   New_Line;
   Put ("X * Y = ");
   Put (X * Y);
   New_Line;
   Put ("X + Y = ");
   Put (X + Y);
   New_Line;
   Put ("X - Y = ");
   Put (X - Y);
   New_Line;
end Demo_Rationals;

这些指南允许您在可维护性和可读性之间保持谨慎的平衡。使用use子句确实可以使代码读起来更像散文文本。但是,维护人员可能还需要解析引用并识别模棱两可的操作。在没有解析这些引用和识别更改 use 子句影响的工具的情况下,完全限定名称是最好的替代方案。

避免use子句会强制您使用完全限定名称。在大型系统中,可能会有许多库单元在with子句中命名。当相应的use子句伴随with子句,并且库包的简单名称被省略(如use子句所允许的那样),对外部实体的引用会被掩盖,并且很难识别外部依赖项。

在某些情况下,use子句的益处是显而易见的。标准包可以与显而易见的假设一起使用,即读者非常熟悉这些包,并且不会引入额外的重载。

use type子句使中缀和前缀运算符都可见,而无需重命名子句。您可以使用use type子句来提高可读性,因为您可以使用更自然的中缀运算符表示法来编写语句。另请参见指南 5.7.2。

您可以通过将use子句放置在包或子程序的主体中,或将其封装在块中以限制可见性来最小化其范围。

命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[编辑 | 编辑源代码]

避免use子句完全会导致枚举文字出现问题,枚举文字必须完全限定。这个问题可以通过声明以枚举文字为值的常量来解决,只是这些常量不能像枚举文字那样重载。

可以在 Rosen (1987) 中找到支持使用 use 子句的论点。

自动化注释

[编辑 | 编辑源代码]

有一些工具可以分析您的 Ada 源代码,解析名称重载,并在use子句或完全限定名称之间自动转换。


Renames 子句

[编辑 | 编辑源代码]
  • 将重命名声明的范围限制为必要的最小范围。
  • 重命名一个长而完全限定的名称以减少复杂性,如果它变得难以处理(参见指南 3.1.4)。
  • 如果此子程序仅仅调用第一个子程序,则使用重命名来提供子程序的主体。
  • 为了可见性目的而进行重命名声明,而不是使用 use 子句,除了运算符(参见指南 5.7.1)。
  • 当您的代码与使用非描述性或不适用的命名法的可重用组件交互时,请重命名部分。
  • 使用项目范围内的标准缩略词列表来重命名常用包。
  • 提供一个use type而不是一个重命名子句来为运算符提供可见性。
procedure Disk_Write (Track_Name : in     Track;
                      Item       : in     Data) renames
   System_Specific.Device_Drivers.Disk_Head_Scheduler.Transmit;

另请参见指南 5.7.1 中的示例,其中包级别的重命名子句为包提供了缩写Rational_Numbers_IO.

如果滥用重命名功能,代码可能难以阅读。一个重命名子句可以将缩写替换为限定符或长包名称的本地。这可以使代码更具可读性,但将代码固定在完整名称上。您可以使用重命名子句来一次评估复杂的名称,或者提供对对象的新“视图”(无论它是否被标记)。但是,使用重命名子句通常可以通过仔细选择名称来避免或使其明显不可取,以便完全限定的名称读起来很好。

当子程序主体调用另一个子程序而不添加本地数据或其他算法内容时,让此子程序主体重命名实际执行工作的子程序会更具可读性。因此,您避免必须编写代码来“通过”子程序调用(原理 1995,第 II.12 节)。

重命名声明列表充当缩写定义列表(参见指南 3.1.4)。作为替代方案,您可以在库级别重命名包以定义包的项目范围内的缩写,然后with重命名的包。通常从重用库中调用的部分没有像可能的那样通用或与新应用程序的命名方案匹配的名称。导出重命名子程序的接口包可以映射到您的项目的命名法。另请参见指南 5.7.1。

Ada 参考手册 1995,第 8.5 节 [带注释的] 中描述的重命名类型的方法是使用子类型(参见指南 3.4.1)。

use type子句消除了重命名中缀运算符的必要性。由于您不再需要显式重命名每个运算符,因此您可以避免错误,例如将+重命名为-。另请参见指南 5.7.1。

命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[编辑 | 编辑源代码]

包名应尽量具有意义,并考虑到包名将在很多地方用作前缀(例如:Pkg.OperationObject : Pkg.Type_Name;)。如果将每个包都重命名为某个缩写,则会失去选择有意义名称的意义,并且难以跟踪所有缩写代表的内容。

为了在 Ada 95 环境中向上兼容 Ada 83 程序,该环境包括 Ada 83 库级包的库级重命名(Ada 参考手册 1995,§J.1 [带注释的])。不建议您在 Ada 95 代码中使用这些重命名。


重载子程序

[edit | edit source]

指南

[edit | edit source]

将重载限制为对不同类型参数执行类似操作的广泛使用子程序(Nissen 和 Wallis 1984)。

示例

[edit | edit source]
function Sin (Angles : in     Matrix_Of_Radians) return Matrix;
function Sin (Angles : in     Vector_Of_Radians) return Vector;
function Sin (Angle  : in     Radians)           return Small_Real;
function Sin (Angle  : in     Degrees)           return Small_Real;

原理

[edit | edit source]

过度重载会让维护人员感到困惑(Nissen 和 Wallis 1984,65)。如果重载成为习惯,还存在隐藏声明的危险。如果参数配置文件不唯一,尝试重载操作实际上可能会隐藏原始操作。从那时起,就无法确定调用新操作是否符合程序员的意愿,或者程序员是否打算调用隐藏的操作,而意外地隐藏了它。

命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[edit | edit source]

本指南并不禁止在不同包中声明具有相同名称的子程序。


重载运算符

[edit | edit source]

指南

[edit | edit source]
  • 保留重载运算符的传统意义(Nissen 和 Wallis 1984)。
  • 使用“+”来标识添加、联接、增加和增强类型的函数。
  • 使用“-“”来标识减法、分离、减少和消耗类型的函数。
  • 当应用于标记类型时,谨慎而一致地使用运算符重载。

示例

[edit | edit source]
function "+" (X : in     Matrix;
              Y : in     Matrix)
  return Matrix;
...
Sum := A + B;

原理

[edit | edit source]

破坏运算符的传统解释会导致代码混乱。

运算符重载的优点是,当使用它时,代码可以变得更清晰,并且可以更紧凑(更易读)地编写。这可以使语义简单自然。但是,很容易误解重载运算符的含义,尤其是在应用于后代时。如果程序员没有应用自然的语义,情况尤其如此。因此,如果无法一致地使用重载,并且很容易误解,请不要使用重载。

命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[edit | edit source]

任何重载都存在潜在问题。例如,如果有几个版本的"+"运算符,并且对其中一个运算符的更改影响其参数的数量或顺序,则查找必须更改的出现的操作可能会很困难。


重载相等运算符

[edit | edit source]

指南

[edit | edit source]
  • 为私有类型定义适当的相等运算符。
  • 考虑重新定义私有类型的相等运算符。
  • 当为类型重载相等运算符时,请维护代数等价关系的属性。

原理

[edit | edit source]

与私有类型一起提供的预定义相等操作取决于用于实现该类型的 数据结构。如果使用访问类型,则相等意味着操作数具有相同的指针值。如果使用离散类型,则相等意味着操作数具有相同的值。如果使用浮点类型,则相等基于 Ada 模型间隔(请参见指南 7.2.7)。因此,您应重新定义相等以提供客户端预期的含义。如果使用访问类型实现私有类型,则应重新定义相等以提供深度相等。对于浮点类型,您可能希望提供一个在某个应用程序相关 epsilon 值内测试相等的相等性。

对私有类型相等含义的任何假设都会在该类型的实现上产生依赖关系。有关详细讨论,请参见 Gonzalez(1991)。

当定义“=”时,此符号隐含了传统的代数含义。正如 Baker(1991)中所述,相等运算符应保持以下属性:

    • 自反a = a
    • 对称a = b ==> b = a
    • 传递a = b 并且 b = c ==> a = c

在重新定义相等时,您不需要具有结果类型为Standard.Boolean。理由(1995,§6.3)给出了两个结果类型为用户定义类型的示例。在三值逻辑抽象中,您重新定义相等以返回以下之一:True, False,或Unknown。在向量处理应用程序中,您可以定义一个返回布尔值向量的逐分量相等运算符。在这两种情况下,您还应重新定义不相等,因为它不是相等函数的布尔补码。



使用异常

[edit | edit source]

Ada 异常是一种增强可靠性的语言特性,旨在帮助在出现错误或意外事件的情况下指定程序行为。异常不打算提供通用控制结构。此外,不应将过度使用异常视为提供完整软件容错的充分条件(Melliar-Smith 和 Randell 1987)。

本节讨论了如何以及何时避免引发异常、如何以及在何处处理异常以及是否应传播异常。有关如何将异常用作单元接口的一部分的信息包括要声明和引发的异常以及在什么条件下引发异常。其他问题在第 4.3 节和第 7.5 节的指南中讨论。


处理与避免异常

[edit | edit source]

指南

[edit | edit source]
  • 如果可以轻松高效地做到这一点,请避免导致引发异常。
  • 为无法避免的异常提供处理程序。
  • 使用异常处理程序通过将错误处理与正常执行分离来增强可读性。
  • 不要使用异常和异常处理程序作为goto语句带来的维护影响。
  • 不要评估由于语言定义的检查失败而变得异常的对象(或对象的一部分)的值。

原理

[edit | edit source]

在许多情况下,可以轻松高效地检测到您即将执行的操作将引发异常。在这种情况下,最好进行检查,而不是允许引发异常并使用异常处理程序进行处理。例如,检查每个指针是否为请记住,记录或数组的任何未重置组件也可能是悬空引用,或者可能承载表示不一致数据的位模式。访问类型的组件始终默认初始化为当遍历由指针连接的记录的链表时。此外,在除以整数之前先测试它是否为 0,并调用询问函数Stack_Is_Empty在调用pop栈包的程序之前。当可以轻松高效地将这些测试作为正在实现的算法的自然组成部分执行时,这些测试是合适的。

然而,提前进行错误检测并不总是那么简单。在某些情况下,这种测试过于昂贵或不可靠。在这种情况下,最好在异常处理程序的范围内尝试操作,以便在异常发生时处理异常。例如,在使用链表实现列表的情况下,在每次调用过程之前调用函数Entry_Exists仅仅为了避免引发异常Modify_Entry效率非常低下Entry_Not_Found. 为了避免异常而搜索列表所花费的时间与执行更新而搜索列表所花费的时间一样多。类似地,在异常处理程序的范围内尝试对实数进行除法以处理数值溢出,要比事先测试被除数是否过大或除数是否过小以便商可以在机器上表示要容易得多。

在并发情况下,提前进行的测试也可能不可靠。例如,如果您想在多用户系统上修改现有文件,最好在异常处理程序的范围内尝试这样做,而不是事先测试文件是否存在,文件是否受到保护,文件系统是否有足够的空间来扩展文件等等。即使您测试了所有可能的错误情况,也不能保证在测试之后和修改操作之前不会有任何变化。您仍然需要异常处理程序,因此提前测试毫无意义。

只要不适用这种情況,正常的和可预测的事件应该由代码处理,而不需要异常所代表的异常控制转移。当异常处理程序中只包含故障处理代码时,这种分离使代码更容易阅读。读者可以跳过所有异常处理程序,仍然理解代码的正常控制流程。出于这个原因,异常永远不应该在同一个单元内被引发和处理,作为一种goto循环,if, case,或语句的循环关联。

退出语句的形式。评估异常对象会导致错误执行(Ada 参考手册 1995,第 13.9.1 节 [注释])。语言定义的检查失败会引发异常。在相应的异常处理程序中,您需要执行适当的清理操作,包括记录错误(参见准则 5.8.2 中关于异常发生的讨论)和/或重新引发异常。评估将您带入异常处理代码的对象会导致错误执行,您不知道您的异常处理程序是否已完全或正确执行。另请参见准则 5.9.1,该准则在Ada.Unchecked_Conversion.


的上下文中讨论了异常对象

[edit | edit source]

指南

[edit | edit source]
  • 在为others编写异常处理程序时,通过Exception_Name, Exception_Message,或Exception_Information捕获并返回有关异常的额外信息,这些子程序在预定义包中声明Ada.Exceptions.
  • 使用others中声明,仅用于捕获不能显式枚举的异常,最好仅用于标记潜在的中止。
  • 在开发过程中,捕获others,捕获正在处理的异常,并考虑为该异常添加一个显式处理程序。

示例

[edit | edit source]

以下简化的示例让用户有机会输入 1 到 3 之间的整数。如果发生错误,它会向用户提供信息。对于超出预期范围的整数值,该函数会报告异常的名称。对于任何其他错误,该函数会提供更完整的跟踪信息。跟踪信息的量是实现相关的。

with Ada.Exceptions;
with Ada.Text_IO;
with Ada.Integer_Text_IO;
function Valid_Choice return Positive is
   subtype Choice_Range is Positive range 1..3;

   Choice : Choice_Range;
begin
   Ada.Text_IO.Put ("Please enter your choice: 1, 2, or 3: ");
   Ada.Integer_Text_IO.Get (Choice);
   if Choice in Choice_Range then   -- else garbage returned
      return Choice;
   end if;
   when Out_of_Bounds : Constraint_Error => 
      Ada.Text_IO.Put_Line ("Input choice not in range.");
      Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Name (Out_of_Bounds));
      Ada.Text_IO.Skip_Line;
   when The_Error : others =>
      Ada.Text_IO.Put_Line ("Unexpected error.");
      Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Information (The_Error));
      Ada.Text_IO.Skip_Line;
end Valid_Choice;

原理

[edit | edit source]

预定义包Ada.Exceptions允许您记录异常,包括其名称和跟踪信息。在为others编写处理程序时,您应该提供有关异常的信息以促进调试。因为您可以访问有关异常发生的信息,所以您可以以标准方式保存适合以后分析的信息。通过使用异常发生,您可以识别特定的异常,并记录详细信息或采取纠正措施。

提供对others的处理程序,使您可以遵循本节中的其他准则。它提供了一个位置来捕获和转换真正意外的异常,这些异常没有被显式处理程序捕获。虽然有可能提供“防火墙”以防止意外异常在没有提供处理程序的情况下传播,但您可以转换出现的意外异常。该others处理程序无法区分不同的异常,因此,任何此类处理程序都必须将异常视为灾难。即使这种灾难仍然可以在此时转换为用户定义的异常。因为对others的处理程序会捕获任何未被显式处理的异常,所以在任务或主子程序的框架中放置一个处理程序可以提供执行最终清理并干净关闭的机会。

others编写处理程序需要谨慎。您应该在处理程序中为它命名(例如,Error : others;),以便区分实际引发的异常或异常引发的确切位置。通常,others处理程序不能对可以做什么或甚至需要“修复”什么做出任何假设。

在开发过程中使用对others的处理程序,当预计异常发生频繁时,可能会阻碍调试,除非您利用Ada.Exceptions中的功能。对于开发人员来说,查看带有实际异常信息的跟踪更有信息量,这些信息由Ada.Exceptions子程序捕获。编写没有这些子程序的处理程序会限制您可能看到的错误信息的量。例如,您可能只会在跟踪中看到转换后的异常,而不会列出引发原始异常的位置。

命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[edit | edit source]

可以使用Exception_Idothers处理程序中区分不同的异常,但这不推荐。类型Exception_Id是实现定义的。操作类型为Exception_Id的值会降低程序的可移植性,并使其更难理解。


传播

[edit | edit source]

指南

[edit | edit source]
  • 处理所有异常,包括用户定义的异常和预定义的异常。
  • 对于可能引发的每个异常,在合适的框架中提供一个处理程序,以防止异常意外传播到抽象之外。

原理

[edit | edit source]

“不可能发生”的说法不是一种可接受的编程方法。您必须假设它可能发生,并且在发生时处于控制之中。您应该为“不可能到达这里”的情况提供防御性代码例程。

一些现有的建议要求捕获和将任何异常传播到调用单元。这种建议可能会停止程序。您应该捕获异常并传播它或一个替代异常,只有当您的处理程序在错误的抽象级别上进行恢复时。进行恢复可能很困难,但替代方案是程序无法满足其规范。

显式请求终止意味着您的代码控制着这种情况,并已确定这是唯一安全的行动方针。处于控制之中可以提供机会以受控方式关闭(清理松散的结,关闭文件,将表面释放到手动控制,发出警报),这意味着所有可用的编程恢复尝试都已经完成。


定位异常的起因

[edit | edit source]

指南

[edit | edit source]
  • 不要依赖于能够识别引发故障的预定义异常或实现定义的异常。
  • 使用Ada.Exceptions中定义的功能来捕获有关异常的尽可能多的信息。
  • 使用块将代码的局部区域与其自己的异常处理程序相关联。

示例

[edit | edit source]

参见准则 5.6.9。

原理

[edit | edit source]

在异常处理程序中,很难确定到底是哪条语句和语句中的哪个操作引发了异常,特别是预定义异常和实现定义的异常。预定义异常和实现定义的异常是转换为更高级别抽象以进行处理的候选对象。用户定义的异常与应用程序更紧密相关,更适合在处理程序中进行恢复。

用户定义的异常也很难本地化。将处理程序与小的代码块关联有助于缩小可能性,从而更容易编写恢复操作。在子程序或任务体内的较小块中放置处理程序,还可以允许在恢复操作后恢复子程序或任务。如果您不在块内处理异常,则处理程序可用的唯一操作是按照准则 5.8.3 关闭任务或子程序。

如准则 5.8.2 所述,您可以记录有关异常的运行时系统信息。您还可以向异常附加消息。在代码开发、调试和维护过程中,此信息应该有助于定位异常的原因。

命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[edit | edit source]

通过块及其异常处理程序来保护您选择的代码部分的最佳大小非常依赖于应用程序。粒度太小,会导致您在为异常操作编程时比为正常算法花费更多精力。粒度太大,会重新引入确定问题出在哪里以及恢复正常流程的问题。



错误执行和有界错误

[edit | edit source]

Ada 95 引入了有界错误类别。有界错误是指行为不确定但处于定义明确的范围内的案例(原理 1995,第 1.4 节)。有界错误的结果是限制编译器的行为,以便在出现错误的情况下,Ada 环境不会随意执行任何操作。《Ada 参考手册 1995,第 1.1.5 节》[Annotated] 定义了一组可能的输出结果,用于处理未定义行为的后果,例如未初始化的值或超出其子类型范围的值。例如,正在执行的程序可能会引发预定义的异常Program_Error, Constraint_Error,或者它可能什么也不做。

当 Ada 程序生成编译器或运行时环境不需要检测的错误时,该程序就是错误的。如《Ada 参考手册 1995,第 1.1.5 节》[Annotated] 中所述,“错误执行的影响是不可预测的”。如果编译器检测到错误程序的实例,它的选择是指示编译时错误;插入代码以引发Program_Error,可能还会写一条相关消息;或者什么也不做。

错误性不是 Ada 独有的概念。以下准则描述或解释了《Ada 参考手册 1995,第 1.1.5 节》[Annotated] 中定义的错误性的某些特定实例。这些准则并非意在面面俱到,而是着重强调一些常被忽视的问题领域。严格来说,任意顺序依赖关系并不属于错误执行的范畴;因此,在准则 7.1.9 中将它们作为可移植性问题进行了讨论。


未经检查的转换

[edit | edit source]

指南

[edit | edit source]
  • 使用Ada.Unchecked_Conversion仅在万不得已的情况下才使用(《Ada 参考手册 1995,第 13.9 节》[Annotated])。
  • 考虑在以下情况下使用'Valid属性检查标量数据的有效性。
  • 确保从Ada.Unchecked_Conversion正确地表示参数子类型的某个值。
  • 隔离Ada.Unchecked_Conversion的使用,放在包体中。

示例

[edit | edit source]

以下示例展示了如何使用'Valid属性检查标量数据的有效性

------------------------------------------------------------------------
with Ada.Unchecked_Conversion;
with Ada.Text_IO;
with Ada.Integer_Text_IO;

procedure Test is

   type Color is (Red, Yellow, Blue);
   for Color'Size use Integer'Size;

   function Integer_To_Color is
      new Ada.Unchecked_Conversion (Source => Integer,
                                    Target => Color);

   Possible_Color : Color;
   Number         : Integer;

begin  -- Test

   Ada.Integer_Text_IO.Get (Number);
   Possible_Color := Integer_To_Color (Number);

   if Possible_Color'Valid then
      Ada.Text_IO.Put_Line(Color'Image(Possible_Color));
   else
      Ada.Text_IO.Put_Line("Number does not correspond to a color.");
   end if;

end Test;
------------------------------------------------------------------------

原理

[edit | edit source]

未经检查的转换是不考虑源类型或目标类型对这些位和位位置的含义的按位复制。源位模式在目标类型的上下文中很容易毫无意义。未经检查的转换会创建违反后续操作类型约束的值。对大小不匹配的对象进行未经检查的转换将产生与实现相关的结果。

使用'Valid属性对标量数据进行检查,可以检查它是否在范围内,如果超出范围也不会引发异常。在以下几种情况下,此类有效性检查可以提高代码的可读性和可维护性

    • 通过未经检查的转换生成的数据
    • 输入数据
    • 从外部语言接口返回的参数值
    • 中止赋值(在异步控制转移期间或执行abort语句期间)
    • 由于语言定义的检查失败而导致的中断赋值
    • 使用'Address属性指定地址的数据

在没有编译器或运行时检查的情况下获得访问值时,不应该假定它正确。在处理访问值时,使用'Valid属性有助于防止使用Ada 环境不需要提供动态分配对象的释放。如果提供,它可能被隐式提供(当它们的访问类型超出范围时,对象会被释放),显式提供(当, Unchecked_Access,或Ada.Unchecked_Conversion.

后可能发生的错误取消引用。在将非标量对象用作未经检查的转换中的实际参数的情况下,应确保其从过程返回时的值正确地表示子类型中的值。这种情况发生在参数处于模式时outin out。在与外部语言接口或使用语言定义的输入过程时,检查值非常重要。《Ada 参考手册 1995,第 13.9.1 节》[Annotated] 列出了有关数据有效性的完整规则。


未经检查的释放

[edit | edit source]

指南

[edit | edit source]
  • 隔离Ada 环境不需要提供动态分配对象的释放。如果提供,它可能被隐式提供(当它们的访问类型超出范围时,对象会被释放),显式提供(当的使用,放在包体中。
  • 确保在退出本地对象的范围后,不存在对本地对象的悬空引用。

原理

[edit | edit source]

准则 5.4.5 中已经给出了大多数使用Ada 环境不需要提供动态分配对象的释放。如果提供,它可能被隐式提供(当它们的访问类型超出范围时,对象会被释放),显式提供(当的原因。使用此功能时,不会进行检查以验证是否只有一个访问路径指向正在释放的存储。因此,任何其他访问路径都不会被请记住,记录或数组的任何未重置组件也可能是悬空引用,或者可能承载表示不一致数据的位模式。访问类型的组件始终默认初始化为。这些其他访问路径的值可能导致错误执行。

如果您的 Ada 环境隐式地使用动态堆存储,但不能完全可靠地回收和重用堆存储,则不应该使用Ada 环境不需要提供动态分配对象的释放。如果提供,它可能被隐式提供(当它们的访问类型超出范围时,对象会被释放),显式提供(当.


Unchecked Access

[edit | edit source]

指南

[edit | edit source]
  • 尽量减少Unchecked_Access属性的使用,最好将其隔离到包体中。
  • 只对寿命/范围为“库级别”的数据使用Unchecked_Access属性。

原理

[edit | edit source]

可访问性规则在编译时以静态方式进行检查(访问参数除外,它们在运行时进行检查)。这些规则确保访问值不会超出其所指定的对象的寿命。由于这些规则在Unchecked_Access的情况下不适用,因此可能会通过访问路径访问不再处于范围内的对象。

隔离Unchecked_Access意味着将其使用与包的客户端隔离。您不应该将其应用于访问值仅仅是为了向客户端返回一个现在不安全的价值。

当您使用属性Unchecked_Access时,您正在以不安全的方式创建访问值。您面临着悬空引用的风险,这反过来会导致执行错误 (Ada 参考手册 1995,§13.9.1 [带注释的])。

例外情况

[编辑 | 编辑源代码]

Ada 参考手册 1995,§13.10 [带注释的]) 为此危险属性定义了以下潜在用途。“此属性提供支持以下情况:当一个局部对象需要被插入到一个全局链接数据结构中时,程序员知道该对象将在退出其作用域之前始终从数据结构中删除。”


地址子句

[编辑 | 编辑源代码]
  • 使用地址子句将变量和条目映射到硬件设备或内存,而不是模拟 FORTRAN 的“等价”功能。
  • 确保在属性定义子句中指定的地址有效,并且不与对齐冲突。
  • 如果您的 Ada 环境中可用,请使用包Ada.Interrupts将处理程序与中断关联。
  • 避免将地址子句用于未导入的程序单元。
Single_Address : constant System.Address := System.Storage_Elements.To_Address(...);
Interrupt_Vector_Table : Hardware_Array;
for Interrupt_Vector_Table'Address use Single_Address;

为多个对象或程序单元指定单个地址的结果是未定义的,与为单个对象或程序单元指定多个地址一样。为中断指定多个地址子句也是未定义的。它不一定覆盖对象或程序单元,也不一定将单个条目与多个中断关联起来。

您有责任确保您指定的地址的有效性。Ada 要求地址对象是其对齐的整数倍数。

在 Ada 83 (Ada 参考手册 1983) 中,您必须使用类型的值System.Address将中断条目附加到中断。虽然这种技术在 Ada 95 中是允许的,但您正在使用一个过时的功能。您应该使用受保护的过程和相应的编译指示 (参考手册 1995,§C.3.2)。


抑制异常检查

[编辑 | 编辑源代码]
  • 在开发期间不要抑制异常检查。
  • 如果需要,在运行期间,引入包含可以安全地移除异常检查的最小语句范围的块。

如果您禁用了异常检查,并且程序执行导致一个原本会引发异常的条件,那么程序执行将是错误的。结果是不可预测的。此外,您仍然必须做好准备,如果这些异常在您调用的子程序、任务和包的体中被引发并从其中传播,则必须处理这些被抑制的异常。

通过最小化移除异常检查的代码,您可以提高程序的可靠性。有一个经验法则表明,20% 的代码占了 80% 的 CPU 时间。因此,一旦您确定了实际需要移除异常检查的代码,明智的做法是将它隔离在一个块中(并添加适当的注释),并将周围的代码保留异常检查。


初始化

[编辑 | 编辑源代码]
  • 在使用之前初始化所有对象。
  • 在初始化访问值时要谨慎。
  • 不要依赖于不属于语言的一部分的默认初始化。
  • 从受控类型派生并覆盖原始过程以确保自动初始化。
  • 在使用实体之前确保其已细化。
  • 在声明中谨慎使用函数调用。

第一个示例说明了初始化访问值的潜在问题

procedure Mix_Letters (Of_String : in out String) is
   type String_Ptr is access String;
   Ptr : String_Ptr := new String'(Of_String);  -- could raise Storage_Error in caller
begin -- Mix_Letters
   ...
exception
   ...  -- cannot trap Storage_Error raised during elaboration of Ptr declaration
end Mix_Letters;

第二个示例说明了确保实体在使用之前细化的重要性

------------------------------------------------------------------------
package Robot_Controller is
   ...
   function Sense return Position;
   ...
end Robot_Controller;
------------------------------------------------------------------------
package body Robot_Controller is
...
   Goal : Position := Sense;       -- This raises Program_Error
   ...
   ---------------------------------------------------------------------
   function Sense return Position is
   begin
      ...
   end Sense;
   ---------------------------------------------------------------------
begin  -- Robot_Controller
   Goal := Sense;                  -- The function has been elaborated.
   ...
end Robot_Controller;
------------------------------------------------------------------------

Ada 除了访问类型之外,没有为任何类型的对象的初始默认值定义初始默认值,访问类型的初始默认值为 null。如果您在声明访问值时初始化它,并且分配引发异常Storage_Error,则异常将在调用过程中而不是被调用过程中引发。调用者没有准备处理此异常,因为它对导致问题分配的原因一无所知。

操作系统在分配内存页时所做的事情会有所不同:一个操作系统可能会将整个页面清零;第二个操作系统可能什么都不做。因此,在对象被赋予值之前使用该对象的值会导致不可预测的行为(但有限制),可能会引发异常。对象可以通过声明隐式初始化,也可以通过赋值语句显式初始化。在声明时初始化是最安全的,也是维护人员最容易理解的。您还可以为记录的组件指定默认值,作为这些记录的类型声明的一部分。

确保初始化并不意味着在声明时初始化。在上面的示例中,目标必须通过函数调用初始化。这不能在声明时发生,因为函数感测尚未细化,但它可以在以后作为封闭包体的语句序列的一部分发生。

在声明(初始化)中调用一个未细化的函数会引发异常,Program_Error,必须在包含声明的单元之外处理。这对函数引发的任何异常都是如此,即使它已被细化。

如果函数调用在声明中引发异常,则不会在该直接作用域中处理该异常。它会被引发到封闭作用域。这可以通过嵌套块来控制。

另请参见指南 9.2.3。

命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

[编辑 | 编辑源代码]

有时,细化顺序可以用编译指示来控制Elaborate_All。编译指示Elaborate_All应用于库单元会导致该单元及其依赖项的传递闭包的细化。换句话说,从该库单元体可达的所有库单元体都会被细化,从而防止在细化之前访问错误 (参考手册 1995,§10.3)。使用编译指示Elaborate_Body当您希望在包声明之后立即细化包体时。


5.9.7 直接 I/O 和顺序 I/O

  • 确保从Ada.Direct_IOAda.Sequential_IO获得的值在范围内。
  • 使用'Valid属性来检查通过Ada.Direct_IOAda.Sequential_IO

原理

获得的标量值的有效性。

[编辑 | 编辑源代码]异常Data_Error可能由Read异常这些包中找到的程序传播,如果读取的元素不能被解释为所需子类型的值(Ada 参考手册 1995,§A.13 [注释])。但是,如果相关的检查过于复杂,实现可能不会传播异常。在读取的元素不能被解释为所需子类型的值,但

命名关联允许在对现有调用的影响最小的情况下向子程序插入新参数。

没有传播的情况下,结果值可能异常,对该值的后续引用会导致错误执行。

[编辑 | 编辑源代码]


有时很难迫使优化编译器对编译器认为在范围内的值执行必要的检查。大多数编译器供应商允许抑制优化的选项,这可能会有所帮助。

异常传播

[编辑 | 编辑源代码]以下代码展示了使用调整防止异常传播到任何用户定义的

原理

过程之外,在每个过程的末尾为所有预定义和用户定义的异常提供处理程序。

[编辑 | 编辑源代码]以下代码展示了使用调整使用Program_Error来传播异常会导致有界错误(Ada 参考手册 1995,§7.6.1 [注释])。要么异常将被忽略,要么


将引发异常。

受保护的对象

[编辑 | 编辑源代码]

原理

不要在受保护的入口、受保护的过程或受保护的函数中调用可能阻塞的操作。

[编辑 | 编辑源代码]

    • Ada 参考手册 1995,§9.5.1 [注释] 列出了可能阻塞的操作语句之后
    • Select语句之后
    • Accept
    • 入口调用语句语句之后
    • Delay语句之后
    • Abort
    • 任务创建或激活
    • 对受保护的子程序(或外部重新排队)的外部调用,其目标对象与受保护操作的目标对象相同

对子程序的调用,其主体包含可能阻塞的操作Program_Error调用受保护入口、过程或函数内的任何这些可能阻塞的操作,可能会导致检测到有界错误或死锁情况。在有界错误的情况下,异常


将被引发。此外,避免在受保护的入口、过程或函数中调用可能直接或间接调用操作系统原语或类似操作的例程,这些操作会导致 Ada 运行时系统无法识别的阻塞。

Abort 语句

原理

不要创建依赖于完全包含在中止延迟操作执行内的主任务的任务。

[编辑 | 编辑源代码]

    • 中止延迟操作是以下操作之一
    • 受保护的入口、受保护的过程或受保护的函数用户定义的Initialize
    • 受保护的入口、受保护的过程或受保护的函数以下代码展示了使用作为受控对象的默认初始化的最后一步使用的过程
    • 受保护的入口、受保护的过程或受保护的函数调整用于受控对象最终化的过程

用于受控对象赋值的过程Program_ErrorAda 参考手册 1995,§9.8 [注释] 指出指南中不鼓励的做法会导致有界错误。如果实现检测到错误,将引发异常abort。如果实现没有检测到错误,操作将按中止延迟操作之外的方式进行。一个



语句本身可能没有效果。

摘要

[编辑 | 编辑源代码]

语法的可选部分
  • 当循环嵌套时,将名称与循环关联(Booch 1986、1987)。
  • 将名称与包含任何exit语句的循环关联。
  • [编辑 | 编辑源代码]
  • 在所有exit来自嵌套循环的语句中使用循环名称。
  • 在包规范和主体末尾包含定义的程序单元名称。
  • 在任务规范和主体末尾包含定义的标识符。
  • accept语句的循环关联。
  • 末尾包含入口标识符。
  • 在子程序主体末尾包含设计器。

当块嵌套时,将名称与块关联。

参数列表
  • [编辑 | 编辑源代码]
  • 用描述性的方式命名形式参数名形式参数,以减少对注释的需求。
  • 在很少使用或具有许多形式参数的子程序或入口的调用中使用命名参数关联。
  • 在实例化泛型时使用命名关联。
  • 当实际参数是任何字面量或表达式时,使用命名关联进行澄清。
  • 提供默认参数,以便偶尔对广泛使用的子程序或条目进行特殊使用。
  • 将默认参数放在形式参数列表的末尾。
  • 考虑为添加到现有子程序的新参数提供默认值。
  • 在很少使用或具有许多形式参数的子程序或入口的调用中使用命名参数关联。
  • 使用适用于您的应用程序的最严格的参数模式。

显示所有过程和入口参数的模式指示(Nissen 和 Wallis 1984)。

类型
  • 通过从现有类型派生新类型来使用现有类型作为构建块。
  • 对子类型使用范围约束。
  • 定义新类型,尤其是派生类型,以包含最大可能的价值集合,包括边界值。
  • 使用子类型限制派生类型的范围,排除边界值。
  • 当没有有意义的组件添加到类型时,使用类型派生而不是类型扩展。
  • 避免使用匿名数组类型。
  • 仅当不存在或无法创建合适的类型,并且数组不会被整体引用(例如,用作子程序参数)时,才为数组变量使用匿名数组类型。
  • 使用访问参数和访问判别式来确保参数或判别式被视为常量。
  • 优先从受控类型派生,而不是使用受限私有类型。
  • 优先使用受限私有类型,而不是私有类型。
  • 优先使用私有类型,而不是非私有类型。
  • 显式导出所需的操作,而不是放宽限制。
  • 使用访问子程序类型来间接访问子程序。
  • 在可能的情况下,使用抽象标记类型和分派,而不是访问子程序类型来实现子程序的动态选择和调用。

[编辑 | 编辑源代码]

数据结构
  • 在声明判别式时,使用尽可能受约束的子类型(即,具有尽可能具体的范围约束的子类型)。
  • 使用带判别的记录,而不是受约束的数组来表示实际值不受约束的数组。
  • [编辑 | 编辑源代码]
  • 使用记录来分组异构但相关的数据。
  • 使用访问类型指向类范围内的类型来实现异构多态数据结构。
  • 使用标记类型和类型扩展,而不是变体记录(与枚举类型和 case 语句结合使用)。
  • 记录结构不应总是扁平的。提取出公共部分。
  • 对于大型记录结构,将相关组件分组到较小的子记录中。
  • 对于嵌套记录,选择在引用内部元素时可读性良好的元素名称。
  • 考虑使用类型扩展来组织大型数据结构。
  • 区分静态数据和动态数据。谨慎使用动态分配的对象。
  • 仅当需要动态创建和销毁动态分配的数据结构,或需要通过不同的名称引用它们时,才使用动态分配的数据结构。
  • 不要丢弃指向未分配对象的指针。
  • 不要留下指向已分配对象的悬空引用。
  • 初始化记录中所有访问变量和组件。
  • 不要依赖内存释放。
  • 显式释放内存。
  • 使用长度子句来指定总分配大小。
  • Storage_Error.
  • 提供处理程序。
  • 使用受控类型来实现操作动态数据的私有类型。
  • 除非您的运行时环境可靠地回收动态堆存储,否则避免使用无约束记录对象。
    • 除非您的运行时环境可靠地回收动态堆存储,否则仅在库包、主子程序或永久任务的最外层、未嵌套的声明部分声明以下项目
    • 访问类型
    • [编辑 | 编辑源代码]
    • 除无约束记录之外的其他无约束复合类型对象
  • 复合对象足够大(在编译时),以便编译器在堆上隐式分配
    • 除非您的运行时环境可靠地回收动态堆存储,或者您正在创建永久的、动态分配的任务,否则请避免在以下情况下声明任务
    • 组件为任务的无约束数组子类型
    • 包含任务数组的组件的判别记录子类型,其中数组大小取决于判别的值
    • 除库包或主子程序的最外层、未嵌套的声明部分之外的任何声明区域
  • 尽量减少对别名变量的使用。
  • 除无约束记录之外的无约束复合类型的对象
  • 当想要隐藏内部连接和簿记信息时,使用别名来引用数据结构的一部分。
  • 使用访问辨别式创建自引用数据结构,即数据结构的其中一个组件指向封闭结构。
  • 对静态创建的参差不齐的数组使用别名(Rationale 1995,§3.7.1)。.

当你创建需要按位操作的数据结构(例如

表达式
  • 使用'第一个'最后一个而不是使用数字字面量来表示范围的第一个或最后一个值。
  • 使用'范围或范围的子类型名称,而不是'第一个 .. '最后一个.
  • 使用数组属性'第一个, '最后一个,或'长度而不是使用数字字面量来访问数组。
  • 使用'范围数组的,而不是索引子类型的名称来表达范围。
  • 使用'范围而不是'第一个 .. '最后一个来表达范围。
  • [编辑 | 编辑源代码]
  • 使用圆括号来指定子表达式的求值顺序,其正确性取决于从左到右的求值。
  • 使用括号指定子表达式求值的顺序以澄清表达式(NASA 1987)。
  • 选择标志的名称,使它们表示可以以肯定形式使用的状态。
  • 使用逻辑运算符的短路形式来指定条件的顺序,当一个条件失败意味着另一个条件将引发异常时。
  • 使用<=>=在包含实数操作数的关系表达式中,而不是=.

语句

避免依赖于使用负数的名称和构造。
  • [编辑 | 编辑源代码]
  • 最大程度地减少嵌套表达式的深度(Nissen 和 Wallis 1984)。
  • 最大程度地减少嵌套控制结构的深度(Nissen 和 Wallis 1984)。
  • 使用切片而不是循环来复制数组的一部分。
  • 尽量减少使用others选择在case语句的循环关联。
  • 中。不要在case语句带来的维护影响。
  • 使用case语句中使用枚举文字的范围,而应使用if/elsif语句(如果可能)。
  • 使用类型扩展和分派,而不是case尝试使用简化启发式方法。
  • 使用for循环,尽可能地使用。
  • 使用while循环,当无法在进入循环之前计算迭代次数,但可以在循环顶部应用简单的延续条件时。
  • 使用带有exit语句的普通循环来处理更复杂的情况。
  • 避免在exit循环中使用whilefor语句。
  • 语句,如果可能的话。
  • 使用exit语句,以增强循环终止代码的可读性 (NASA 1987)。
  • 使用exit when ...而不是if ... then exit尽可能地使用 (NASA 1987)。
  • 复查exit语句放置。
  • 最大程度地减少退出循环的方式。
  • 考虑指定循环的界限。
  • 不要使用goto语句带来的维护影响。
  • 最小化return语句 从子程序 (NASA 1987) 中退出。
  • 突出显示return语句使用注释或空白,以防止它们在其他代码中丢失。
  • 使用块来局部化声明的范围。
  • 使用块来执行局部重命名。
  • 使用块来定义局部异常处理程序。
  • 考虑指定递归的界限。
  • 使用聚合而不是一系列赋值来将值分配给记录的所有组件
  • 仅在参数存在常规排序时使用位置关联。

使用聚合而不是临时变量来构建要作为实际参数传递的记录

[编辑 | 编辑源代码]
  • 当您需要为运算符提供可见性时,请使用use type子句的类似指南。
  • 避免/最小化使用use子句 (Nissen 和 Wallis 1984)。
  • 考虑使用包重命名子句而不是use子句用于包。
  • 考虑在以下情况下使用use子句
    • 当需要标准包且没有引入歧义引用时
    • 当需要对枚举文字的引用时
  • 本地化所有use子句的效果。
  • 将重命名声明的范围限制为必要的最小范围。
  • 如果完全限定的长名称变得笨拙,请重命名以减少复杂性。
  • 如果此子程序仅仅调用第一个子程序,则使用重命名来提供子程序的主体。
  • 为了可见性目的,请重命名声明,而不是使用 use 子句,运算符除外。
  • 当您的代码与使用非描述性或不适用的命名法的可重用组件交互时,请重命名部分。
  • 使用项目范围内的标准缩略词列表来重命名常用包。
  • 提供一个use type而不是一个重命名子句来为运算符提供可见性。
  • 将重载限制为对不同类型参数执行类似操作的广泛使用子程序(Nissen 和 Wallis 1984)。
  • 保留重载运算符的传统意义(Nissen 和 Wallis 1984)。
  • 使用“+”来标识添加、联接、增加和增强类型的函数。
  • 使用“-“”来标识减法、分离、减少和消耗类型的函数。
  • 当应用于标记类型时,谨慎而一致地使用运算符重载。
  • 为私有类型定义适当的相等运算符。
  • 考虑重新定义私有类型的相等运算符。
  • 当为类型重载相等运算符时,请维护代数等价关系的属性。

使用异常

[编辑 | 编辑源代码]
  • 如果可以轻松高效地做到这一点,请避免导致引发异常。
  • 为无法避免的异常提供处理程序。
  • 使用异常处理程序通过将错误处理与正常执行分离来增强可读性。
  • 不要使用异常和异常处理程序作为goto语句带来的维护影响。
  • 不要评估由于语言定义的检查失败而变得异常的对象(或对象的一部分)的值。
  • 在为others编写异常处理程序时,通过Exception_Name, Exception_Message,或Exception_Information捕获并返回有关异常的额外信息,这些子程序在预定义包中声明Ada.Exceptions.
  • 使用others中声明,仅用于捕获不能显式枚举的异常,最好仅用于标记潜在的中止。
  • 在开发过程中,捕获others,捕获正在处理的异常,并考虑为该异常添加一个显式处理程序。
  • 处理所有异常,包括用户定义的异常和预定义的异常。
  • 对于可能引发的每个异常,在合适的框架中提供一个处理程序,以防止异常意外传播到抽象之外。
  • 不要依赖于能够识别引发故障的预定义异常或实现定义的异常。
  • 使用Ada.Exceptions中定义的功能来捕获有关异常的尽可能多的信息。
  • 使用块将代码的局部区域与其自己的异常处理程序相关联。

错误执行和有界错误

[编辑 | 编辑源代码]
  • 使用Ada.Unchecked_Conversion仅在万不得已的情况下才使用(《Ada 参考手册 1995,第 13.9 节》[Annotated])。
  • 考虑在以下情况下使用'Valid属性用于检查标量数据的有效性。
  • 确保从Ada.Unchecked_Conversion正确地表示参数子类型的某个值。
  • 隔离Ada.Unchecked_Conversion的使用,放在包体中。
  • 隔离Ada 环境不需要提供动态分配对象的释放。如果提供,它可能被隐式提供(当它们的访问类型超出范围时,对象会被释放),显式提供(当的使用,放在包体中。
  • 确保在退出本地对象的范围后,不存在对本地对象的悬空引用。
  • 尽量减少Unchecked_Access属性的使用,最好将其隔离到包体中。
  • 只对寿命/范围为“库级别”的数据使用Unchecked_Access属性。
  • 使用地址子句将变量和条目映射到硬件设备或内存,而不是模拟 FORTRAN 的“等价”功能。
  • 确保在属性定义子句中指定的地址有效,并且不与对齐冲突。
  • 如果您的 Ada 环境中可用,请使用包Ada.Interrupts将处理程序与中断关联。
  • 避免将地址子句用于未导入的程序单元。
  • 在开发期间不要抑制异常检查。
  • 如果需要,在运行期间,引入包含可以安全地移除异常检查的最小语句范围的块。
  • 在使用之前初始化所有对象,包括访问值。
  • 在初始化访问值时要谨慎。
  • 不要依赖于不属于语言的一部分的默认初始化。
  • 从受控类型派生并覆盖原始过程以确保自动初始化。
  • 在使用实体之前确保其已细化。
  • 在声明中谨慎使用函数调用。
  • 确保从Ada.Direct_IOAda.Sequential_IO获得的值在范围内。
  • 使用'Valid属性来检查通过Ada.Direct_IOAda.Sequential_IO
  • [编辑 | 编辑源代码]以下代码展示了使用调整防止异常传播到任何用户定义的
  • [编辑 | 编辑源代码]
  • 不要在中止延迟操作中使用异步 select 语句。
  • 语句。
华夏公益教科书