跳转到内容

Ada 风格指南/可重用性

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

可移植性 · 面向对象特性

可重用性是指代码可以在不同的应用程序中以最小的修改被使用的程度。当代码在新的应用程序中被重用时,该新应用程序部分继承了该代码的属性。如果代码是可维护的,则应用程序更易于维护。如果它是可移植的,那么应用程序更易于移植。因此,本章的指南在应用本书中的所有其他指南时最有用。几个指南针对可维护性问题。可维护的代码易于更改以满足新的或不断变化的需求。可维护性在重用中发挥着特殊作用。当试图重用代码时,通常需要更改代码以适合新的应用程序。如果代码不能轻易更改,那么它就不太可能被重用。

软件重用涉及许多问题:是否重用部分,如何在库中存储和检索可重用部分,如何认证部分,如何最大化重用的经济价值,如何激励工程师和整个公司重用部分而不是重新发明它们,等等。本章忽略了这些管理、经济和物流问题,而是专注于如何在 Ada 中编写软件部分以提高重用潜力的单个技术问题。其他问题同样重要,但超出了本书的范围。

Ada 的设计目标之一是促进可重用部分的创建和使用,以提高生产力。为此,Ada 提供了开发可重用部分并在可用后对其进行调整的功能。包、可见性控制和单独编译支持模块化和信息隐藏(参见第 4.1、4.2、5.3 和 5.7 节中的指南)。这允许分离代码的应用程序特定部分,最大化适合重用的通用部分,并允许在模块内隔离设计决策,从而促进更改。Ada 类型系统支持数据定义的本地化,以便一致的更改易于进行。Ada 继承特性支持类型扩展,以便可以为应用程序定制数据定义和接口。泛型单元直接支持开发通用、可适应的代码,这些代码可以实例化为执行特定功能。Ada 95 对面向对象技术和抽象的改进支持上述所有目标。仔细使用这些特性并符合本书中的指南,会产生更有可能被重用的代码。

可重用代码的开发方式多种多样。代码可以从以前的项目中获取。可以从头开始为特定领域(如数学库)开发一个可重用的代码库。可重用代码可以作为特定应用程序的有意副产品而开发。可重用代码的开发方式可能会有所不同,因为设计方法需要它。本指南适用于所有这些情况。

经验丰富的程序员认识到,软件重用更多地是一个需求和设计问题,而不是编码问题。本节中的指南旨在在一个开发可重用代码的整体方法中发挥作用。本节将不涉及设计、测试等方面的内容。有关与 Ada 语言相关的重用问题的研究,可以在 AIRMICS(1990 年)、Edwards(1990 年)和 Wheeler(1992 年)中找到。

  • 无论开发方法如何,经验表明,可重用代码具有某些特征,本章做出以下假设。
  • 可重用部分必须易于理解。可重用部分应该是清晰的典范。对可重用部分进行注释的要求甚至比对特定应用程序部分的要求更加严格。
  • 可重用部分必须具有最高质量。它们必须正确、可靠且健壮。可重用部分中的错误或弱点可能会产生深远的影响,因此,其他程序员必须对任何提供重用的部分有高度的信心。
  • 可重用部分必须具有适应性。为了最大限度地发挥其重用潜力,可重用部分必须能够适应各种用户的需求。
  • 可重用部分应该独立。应该能够重用单个部分,而无需采用许多看似无关的其他部分。

除了这些标准之外,可重用部分必须比重新发明更容易重用,必须高效,并且必须可移植。如果重用一个部分比从头开始创建一个部分需要更多的努力,或者重用的部分效率不够高,那么重用就不会轻易发生。有关可移植性的指南,请参见第 7 章。本章不应孤立地阅读。在很多方面,一个写得好的、可重用的组件仅仅是一个写得好的组件的极端例子。前几章和第 9 章中的所有指南都适用于可重用组件以及特定于单个应用程序的组件。随着对 1995 年 Ada 标准修订版的经验不断积累,新的指南可能会出现,而其他指南可能会发生变化。此处列出的指南专门适用于可重用组件。

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

理解和清晰度

[edit | edit source]

对于旨在重用的部分,尤其重要的是它们应该易于理解。部分的作用、使用方法、将来可能进行的预期更改以及工作原理是必须从注释和代码本身的检查中直接显现的事实。为了最大限度地提高可重用部分的可读性,请遵循第 3 章中的指南,其中一些指南在下面重复强调。

与应用程序无关的命名

[edit | edit source]

指南

[edit | edit source]
  • 为可重用部分及其标识符选择限制最少的名称。
  • 选择通用名称以避免与通用实例的命名约定冲突。
  • 使用表示可重用部分的行为特征及其抽象的名称。

示例

[edit | edit source]

通用堆栈抽象

------------------------------------------------------------------------
generic
   type Item is private;
package Bounded_Stack is
   procedure Push (New_Item    : in     Item);
   procedure Pop  (Newest_Item :    out Item);
   ...
end Bounded_Stack;
------------------------------------------------------------------------

适当地重命名以供当前应用程序使用

with Bounded_Stack;

...

   type Tray is ...
   package Tray_Stack is 
      new Bounded_Stack (Item => Tray);

基本原理

[edit | edit source]

为可重用部分选择一个通用或与应用程序无关的名称,可以鼓励其广泛重用。当该部分在特定上下文中使用时,可以对其进行实例化(如果是泛型)或使用更具体的名称重新命名。

当存在一个明显的、最简单、最清晰的名称选择用于可重用部分时,最好将该名称留给该部分的重用者使用,为可重用部分选择一个更长、更具描述性的名称。因此,对于泛型堆栈包来说,Bounded_Stack 比 Stack 更好的名称,因为它将更简单的名称 Stack 留给实例化使用。

在可重用部分的名称中包含行为特征的指示(但不包含实现的指示),以便具有相同抽象(例如,多个堆栈包)但具有不同限制(有界、无界等)的多个部分可以存储在同一个 Ada 库中并用作同一个 Ada 程序的一部分。

缩写

[edit | edit source]

指南

[edit | edit source]
  • 在标识符或单元名称中不要使用缩写。

示例

[edit | edit source]
------------------------------------------------------------------------
with Ada.Calendar;
package Greenwich_Mean_Time is
   function Clock return Ada.Calendar.Time;
   ...
end Greenwich_Mean_Time;
------------------------------------------------------------------------

以下缩写可能在应用程序中使用时不清楚

with Ada.Calendar;
with Greenwich_Mean_Time;
...
   function Get_GMT return Ada.Calendar.Time renames
          Greenwich_Mean_Time.Clock;

基本原理

[edit | edit source]

这是一条比指南 3.1.4 更强的指南。无论注释得多么好,缩写都可能在一些未来的重用上下文中造成混乱。即使是普遍接受的缩写,如 GMT 代表格林威治标准时间,也会造成问题,应该谨慎使用。

本指南与指南 3.1.4 之间的区别在于领域问题。当领域定义明确时,在该领域中接受的缩写和首字母缩略词将阐明应用程序的含义。当同一代码从其特定于域的上下文中删除时,这些缩写可能变得毫无意义。

在上面的示例中,包 Greenwich_Mean_Time 可以用于任何应用程序,而不会丢失含义。但函数 Get_GMT 很容易与不同领域中的其他首字母缩略词混淆。

注释

[edit | edit source]

请参见指南 5.7.2,了解重新命名子句的正确用法。如果特定应用程序广泛使用 Greenwich_Mean_Time 领域,则可以在该应用程序中将包 GMT 重新命名为 GMT

with Greenwich_Mean_Time;
...
   package GMT renames Greenwich_Mean_Time;

泛型形式参数

[edit | edit source]

指南

[edit | edit source]
  • 像文档化任何包规范一样,文档化泛型形式参数的预期行为。

示例

[edit | edit source]

以下示例显示了如何开发非常通用的算法,但必须对其进行清晰的文档化才能使用。

------------------------------------------------------------------------
generic
   -- Index provides access to values in a structure.  For example,
   -- an array, A.
   type Index is (<>);
   type Element is private;
   type Element_Array is array (Index range <>) of Element;
   -- The function, Should_Precede, does NOT compare the indexes
   -- themselves; it compares the elements of the structure.
   -- The function Should_Precede is provided rather than a "Less_Than" function
   -- because the sort criterion need not be smallest first.
   with function Should_Precede (Left  : in     Element;
                                 Right : in     Element)
     return Boolean;
   -- This procedure swaps values of the structure (the mode won't
   -- allow the indexes themselves to be swapped!)
   with procedure Swap (Index1 : in     Index;
                        Index2 : in     Index;
                        A      : in out Element_Array);
   -- After the call to Quick_Sort, the indexed structure will be
   -- sorted:
   --     For all i,j in First..Last :  i<j  =>  A(i) < A(j).
procedure Quick_Sort (First : in     Index := Index'First;
                      Last  : in     Index := Index'Last);
------------------------------------------------------------------------

基本原理

[edit | edit source]

泛型功能是 Ada 的一个最强大的特性,因为它具有形式化。但是,并非所有关于泛型形式参数的假设都可以在 Ada 中直接表达出来。泛型的任何用户都必须准确地了解该泛型需要什么才能正确运行。

在某种意义上,泛型规范是一份合同,实例化者必须提供形式参数,作为回报,他们将获得规范的工作实例。当合同完整并明确所有假设时,双方都能得到最好的服务。

健壮性

[edit | edit source]

以下指南提高了 Ada 代码的健壮性。很容易编写依赖于你没有意识到你正在做出的假设的代码。当这样的部分在不同的环境中重用时,它可能会意外地中断。本节中的指南展示了 Ada 代码如何才能自动符合其环境,以及如何才能使其检查假设违规情况。最后,还给出了一些指南,以警告您 Ada 无法像您希望的那样及时捕获的错误。

命名数字

[edit | edit source]

指南

[edit | edit source]
  • 使用命名的数字和静态表达式,可以让多个依赖关系链接到少量符号。

示例

[edit | edit source]
------------------------------------------------------------------------
procedure Disk_Driver is
   -- In this procedure, a number of important disk parameters are
   -- linked.
   Number_Of_Sectors  : constant :=     4;
   Number_Of_Tracks   : constant :=   200;
   Number_Of_Surfaces : constant :=    18;
   Sector_Capacity    : constant := 4_096;
   Track_Capacity   : constant := Number_Of_Sectors  * Sector_Capacity;
   Surface_Capacity : constant := Number_Of_Tracks   * Track_Capacity;
   Disk_Capacity    : constant := Number_Of_Surfaces * Surface_Capacity;
   type Sector_Range  is range 1 .. Number_Of_Sectors;
   type Track_Range   is range 1 .. Number_Of_Tracks;
   type Surface_Range is range 1 .. Number_Of_Surfaces;
   type Track_Map   is array (Sector_Range)  of ...;
   type Surface_Map is array (Track_Range)   of Track_Map;
   type Disk_Map    is array (Surface_Range) of Surface_Map;
begin  -- Disk_Driver
   ...
end Disk_Driver;
------------------------------------------------------------------------

基本原理

[edit | edit source]

为了重用使用命名的数字和静态表达式的软件,只需要重置一个或少量常量,所有声明和相关代码就会自动更改。除了简化重用,这还减少了出错的机会,并使用不易出错的注释记录了类型和常量的含义。

无约束数组

[edit | edit source]

指南

[edit | edit source]
  • 对于数组形式参数和数组返回值,使用无约束数组类型。
  • 在适当的情况下,使局部变量的大小取决于实际参数的大小。

示例

[edit | edit source]
   ...
   type Vector is array (Vector_Index range <>) of Element;
   type Matrix is array
           (Vector_Index range <>, Vector_Index range <>) of Element;
   ...
   ---------------------------------------------------------------------
   procedure Matrix_Operation (Data : in     Matrix) is
      Workspace   : Matrix (Data'Range(1), Data'Range(2));
      Temp_Vector : Vector (Data'First(1) .. 2 * Data'Last(1));
   ...
   ---------------------------------------------------------------------

基本原理

[edit | edit source]

无约束数组可以声明为其大小取决于形式参数的大小。当用作局部变量时,它们的大小会随着提供的实际参数自动更改。此功能可用于帮助适应部分,因为局部变量中必要的大小更改会自动处理。

最小化和记录假设

[edit | edit source]

指南

[edit | edit source]
  • 最小化单元做出的假设数量。
  • 对于无法避免的假设,使用子类型或约束来自动强制一致性。
  • 对于无法通过子类型自动强制执行的假设,请在代码中添加显式检查。
  • 记录所有假设。
  • 如果代码依赖于特定特殊需求附件的实现才能正常运行,请在代码中记录此假设。

示例

[edit | edit source]

以下写得不好的函数记录了但没有检查其假设

   -- Assumption:  BCD value is less than 4 digits.
   function Binary_To_BCD (Binary_Value : in     Natural)
     return BCD;

下一个示例强制执行与假设的一致性,使检查自动化并将注释变为不必要

   subtype Binary_Values is Natural range 0 .. 9_999;
   function Binary_To_BCD (Binary_Value : in     Binary_Values)
     return BCD;

下一个示例明确检查并记录其假设

   ---------------------------------------------------------------------
   -- Out_Of_Range raised when BCD value exceeds 4  digits.
   function Binary_To_BCD (Binary_Value : in     Natural)
     return BCD is
      Maximum_Representable : constant Natural := 9_999;
   begin  -- Binary_To_BCD
      if Binary_Value > Maximum_Representable then
         raise Out_Of_Range;
      end if;
      ...
   end Binary_To_BCD;
   ---------------------------------------------------------------------

基本原理

[edit | edit source]

任何打算在其他程序中再次使用的部分,尤其是其他程序很可能由其他人编写,都应该是健壮的。它应该通过定义其接口来强制执行尽可能多的假设,并通过在任何无法通过接口强制执行的假设上添加显式防御性检查来防御误用。通过记录对特殊需求附件的依赖关系,您可以警告用户他应该只在提供必要支持的编译环境中重用组件。

注释

[edit | edit source]

您可以通过仔细选择或构造形式参数的子类型来限制输入值的范围。当您这样做时,编译器生成的检查代码可能比您可能编写的任何检查都更有效。事实上,这种检查是语言中强类型化的意图的一部分。但是,对于用户选择参数类型的泛型单元来说,这提出了一个挑战。您的代码必须构建为处理用户可能选择为实例化选择的任何子类型的任何值。

泛型规范中的子类型

[edit | edit source]

指南

[edit | edit source]
  • 在声明模式为 in out 的泛型形式对象时,使用第一个子类型。
  • 注意在声明泛型形式子程序的参数或返回值时使用子类型作为子类型标记。
  • 使用属性而不是字面量。

示例

[edit | edit source]

在以下示例中,似乎为泛型形式对象 Object 提供的任何值都将被约束在 1..10 的范围内。它还似乎在任何实例化中运行时传递给 Put 例程的参数以及 Get 例程返回的值也将受到类似约束

   subtype Range_1_10 is Integer range 1 .. 10;
   ---------------------------------------------------------------------
   generic
      Object : in out Range_1_10;
      with procedure Put (Parameter : in     Range_1_10);
      with function  Get return Range_1_10;
   package Input_Output is
      ...
   end Input_Output;
   ---------------------------------------------------------------------

但是,情况并非如此。鉴于以下合法实例化

   subtype Range_15_30 is Integer range 15 .. 30;
   Constrained_Object : Range_15_30 := 15;
   procedure Constrained_Put (Parameter : in     Range_15_30);
   function  Constrained_Get return Range_15_30;
   package Constrained_Input_Output is
      new Input_Output (Object => Constrained_Object,
                        Put    => Constrained_Put,
                        Get    => Constrained_Get);
   ...

Object、Parameter 和 Get 的返回值被约束在 15..30 范围内。因此,例如,如果泛型包的正文包含赋值语句

Object := 1;

当执行此实例化时,将引发 Constraint_Error。

基本原理

[edit | edit source]

该语言规定,当对泛型形式对象和泛型形式子程序的参数以及返回值执行约束检查时,将强制执行实际子类型的约束(而不是形式子类型)(Ada 语言参考手册 1995, §§12.4 [Annotated] 和 §12.6 [Annotated])。因此,在形式 in out 对象参数中指定的子类型以及在形式子程序配置文件中指定的子类型不需要与实际对象或子程序的子类型匹配。

因此,即使使用已经实例化并测试过多次的泛型单元,以及在实例化时没有报告任何错误的实例化,也可能出现运行时错误。因为泛型形式的子类型约束被忽略,Ada 语言参考手册 (1995, §§12.4 [Annotated] 和 §12.6 [Annotated]) 建议在这些地方使用基本类型的名称以避免混淆。即使这样,您也必须小心,不要假设可以使用基本类型的任何值,因为实例化会强制执行泛型实际参数的子类型约束。为了安全起见,始终通过包含 'First、'Last、'Pred 和 'Succ 之类的属性的符号表达式来引用类型的特定值,而不是通过字面量。

对于泛型,属性提供了保持通用性的方法。可以使用字面量,但字面量存在违反某些约束的风险。例如,假设数组的索引从 1 开始可能会在泛型实例化为基于零的数组类型时造成问题。

注释

[edit | edit source]

添加一个定义泛型形式对象子类型的泛型形式参数并不能解决上述理由中讨论的约束检查规则的影响。您可以使用任何允许的子类型实例化泛型形式类型,并且您不能保证此子类型是第一个子类型

generic
   type Object_Range is range <>;
   Objects : in out Object_Range;
   ...
package X is
   ...
end X;

您可以使用任何 Integer 子类型实例化子类型 Object_Range,例如 Positive。但是,实际变量 Object 可以是 Positive'Base,即 Integer,并且其值不能保证大于 0。

泛型单元中的重载

[编辑 | 编辑源代码]
  • 谨慎对待重载同一个泛型包导出的子程序的名称。
------------------------------------------------------------------------
generic
   type Item is limited private;
package Input_Output is
   procedure Put (Value : in     Integer);
   procedure Put (Value : in     Item);
end Input_Output;
------------------------------------------------------------------------

基本原理

[编辑 | 编辑源代码]

如果上面的示例中显示的泛型包被实例化为 Integer(或 Integer 的任何子类型)作为与泛型形式参数 Item 相对应的实际类型,那么两个 Put 过程具有相同的接口,并且对 Put 的所有调用都是模棱两可的。因此,此包不能与类型 Integer 一起使用。在这种情况下,最好为所有子程序指定明确的名称。请参阅 Ada 参考手册 1995,第 12.3 节 [注释] 获取更多信息。

隐藏的任务

[编辑 | 编辑源代码]
  • 在规范中,记录任何通过 with'ing 规范并使用规范的任何部分而被激活的任务。
  • 记录从泛型单元中隐藏的任务中访问哪些泛型形式参数。
  • 记录任何多线程组件。

基本原理

[编辑 | 编辑源代码]

当可重用代码进入实时系统领域时,任务的影响成为一个主要因素。即使任务可能用于其他目的,它们对调度算法的影响仍然是一个问题,必须清楚地记录下来。有了清楚记录的任务,实时程序员就可以分析性能、优先级等以满足时间要求,或者必要时修改甚至重新设计组件。

必须仔细规划对数据结构的并发访问以避免错误,特别是对于非原子数据结构(有关详细信息,请参阅第 6 章)。如果泛型单元从包含在泛型单元中的任务内访问其泛型形式参数之一(读取或写入泛型形式对象的值,或调用读取或写入数据的泛型形式子程序),那么就有可能发生并发访问,用户可能没有计划。在这种情况下,应该在泛型规范中的注释中提醒用户。

  • 将异常从可重用部分传播出去。仅当您确定处理在所有情况下都合适时,才在可重用部分内处理异常。
  • 在执行泛型形式子程序正确操作的任何必要清理后,传播泛型形式子程序引发的异常。
  • 在引发异常时,将状态变量置于有效状态。
  • 在引发异常时,将参数保持不变。
------------------------------------------------------------------------
generic
   type Number is limited private;
   with procedure Get (Value :    out Number);
procedure Process_Numbers;

------------------------------------------------------------------------
procedure Process_Numbers is
   Local : Number;
   procedure Perform_Cleanup_Necessary_For_Process_Numbers is separate;
   ...
begin  -- Process_Numbers
   ...
   Catch_Exceptions_Generated_By_Get:
      begin
         Get (Local);
      exception
         when others =>
            Perform_Cleanup_Necessary_For_Process_Numbers;
            raise;
      end Catch_Exceptions_Generated_By_Get;
   ...
end Process_Numbers;
------------------------------------------------------------------------

基本原理

[编辑 | 编辑源代码]

在大多数情况下,引发异常是因为发生了不希望发生的事件(例如浮点溢出)。这些事件通常需要以完全不同的方式处理,使用特定软件部分的不同方法。很难预测用户希望如何处理异常的所有方式。将异常传出该部分是最安全的处理方式。

特别是,当泛型形式子程序引发异常时,泛型单元无法理解原因或知道要采取什么纠正措施。因此,此类异常应始终传播回泛型实例化的调用者。但是,泛型单元必须首先清理自身,将其内部数据结构恢复到正确状态,以便在调用者处理当前异常后可以对其进行未来的调用。出于这个原因,所有对泛型形式子程序的调用都应该在 when others 异常处理程序的范围内,如果内部状态被修改,如上面的示例所示。

当调用可重用部分时,该部分的用户应该能够准确地知道已执行了什么操作(在适当的抽象级别)。为了实现这一点,可重用部分必须始终执行其指定功能的所有操作或都不执行;它永远不能做一半。因此,任何通过引发或传播异常而提前终止的可重用部分都应该返回给调用者,对内部或外部状态没有影响。实现这一点的最简单方法是在进行任何状态更改(修改内部状态变量、调用其他可重用部分以修改其状态、更新文件等)之前测试所有可能的异常条件。当这不可能时,最好在引发或传播异常之前将所有内部和外部状态恢复为调用该部分时当前的值。即使这不可能,也必须在该部分规范的注释标题中记录这种潜在的危险情况。

当引发异常时,模式为 out 或 in out 的参数也会出现类似的问题。Ada 语言区分“按值传递”和“按引用传递”参数传递。在某些情况下,需要“按值传递”;在其他情况下,需要“按引用传递”;在剩余的情况下,允许使用任一机制。潜在问题出现在语言未指定要使用的参数传递机制的那些情况下。当引发异常时,不会发生复制回,但对于按引用传递参数的 Ada 编译器(在允许选择的情况下),实际参数已经更新。当参数按值传递时,更新不会发生。为了减少歧义,提高可移植性,并避免在引发异常时,某些实际参数被更新,而另一些没有被更新的情况,最好将 out 和 in out 参数的值视为状态变量,仅在确定不会引发异常后才更新它们。另请参阅指南 7.1.8。

可重用部分的范围可以从低级构建块(例如,数据结构、排序算法、数学函数)到大型可重用子系统。构建块的级别越低,可重用部分就越不可能知道如何处理异常或产生有意义的结果。因此,低级部分应该传播异常。但是,大型可重用子系统应该能够独立于其重用范围内的差异来处理任何预期的异常。

适应性

[编辑 | 编辑源代码]

可重用部分通常需要在特定应用程序中使用之前进行更改。它们应该被结构化,以便更改容易,并且尽可能地局部化。实现适应性的一种方法是创建具有完整功能的通用部分,其中只有一部分可能在给定的应用程序中需要。实现适应性的另一种方法是使用 Ada 的泛型构造来生成可以适当地实例化不同参数的部分。这两种方法都避免了通过更改代码来适应部分的容易出错的过程,但它们有局限性,并且可能带来一些开销。应该尽可能地提供预期的更改,即开发人员可以合理预见到的更改。无法预见的更改只能通过仔细地结构化一个可适应的部分来适应。许多与可维护性相关的考虑因素都适用。如果代码质量高,清晰,并且符合信息隐藏等公认的设计原则,那么在无法预见的情况下进行适应就更容易。

完整的功能

[编辑 | 编辑源代码]
  • 在可重用部分或一组部分中提供核心功能,以便可以在此抽象级别中对其进行有意义的扩展,使其能够被重复使用。
  • 更具体地说,为每个可能包含动态数据的的数据结构提供初始化和终结过程。
  • 对于需要初始化和终结的数据结构,请考虑尽可能从类型 Ada.Finalization.Controlled 或 Ada.Finalization.Limited_Controlled 派生它们。
   Incoming : Queue;
   ...
   Set_Initial (Incoming);     -- initialization operation
   ...
   if Is_Full (Incoming) then  -- query operation
      ...
   end if;
   ...
   Clean_Up (Incoming);        -- finalization operation

基本原理

[编辑 | 编辑源代码]

此功能在设计/编程抽象时尤为重要。您需要在抽象的完整性和可扩展性之间取得平衡。完整性确保您正确地配置了抽象,没有关于其执行环境的内置假设。它还确保函数的适当分离,以便它们对当前应用程序有用,并且在其他组合中对其他应用程序有用。可扩展性确保重用者可以通过扩展添加功能,使用标记类型层次结构(参见指南 8.4.8 和第 9 章)或子库包(参见指南 4.1.6、8.4.1 和 9.4.1)。

在设计重用时,您需要从干净的抽象角度思考。如果您提供的功能太少,并且依赖于重用者扩展抽象,他们可能会遇到缺乏凝聚力的抽象。这种混杂的抽象继承了许多操作,并非所有操作都是必要的,也不都是协同工作的。

当可重用部分可以使用动态数据合理地实现时,任何必须控制内存的应用程序都可以使用初始化和终结例程来防止内存泄漏。然后,如果数据结构变得动态,对这些问题敏感的应用程序可以轻松地进行调整。

预定义类型 Ada.Finalization.Controlled 或 Ada.Finalization.Limited_Controlled 提供自动的、用户定义的初始化、调整和终结过程。当您声明受控类型和对象时,您可以保证编译器将插入必要的初始化、调整和终结调用,从而使您的代码更不容易出错,更易于维护。当覆盖受控类型上的 Initialize 和 Finalize 例程时,请确保调用父 Initialize 或 Finalize。

此示例说明了结束条件函数。抽象应该在用户有机会破坏它之前自动初始化。如果不可能,则应为它提供初始化操作。在任何情况下,它都需要终结操作。提供初始化和终结操作的一种方法是从预定义类型 Ada.Finalization.Controlled 或 Ada.Finalization.Limited_Controlled 派生抽象。在任何可能的情况下,应提供查询操作来确定何时即将超过限制,以便用户可以避免导致异常被引发。

为许多对象提供重置操作也很有用。要了解重置和初始化可能有所不同,请考虑个人计算机上“热启动”和“冷启动”的类似情况。

即使并非所有这些操作都适合抽象,考虑这些操作的练习有助于形成一组完整的操作,其中其他操作可能被另一个应用程序使用。

语言的一些实现将包的所有子程序链接到可执行文件中,而不管它们是否被使用,这使得未使用的操作成为负担(参见指南 8.4.5)。在这种情况下,如果开销很大,请创建功能齐全部分的副本,并用注释注释掉未使用的操作,并说明它们在此应用程序中是多余的。

泛型单元

[编辑 | 编辑源代码]
  • 使用泛型单元避免代码重复。
  • 为泛型单元参数化以实现最大适应性。
  • 重用泛型单元的常见实例,以及泛型单元本身。

基本原理

[编辑 | 编辑源代码]

Ada 不允许在执行期间将数据类型作为实际参数传递给子程序。此类参数必须在实例化泛型单元时指定为泛型形式参数。因此,如果您想编写一个子程序,该子程序在调用时对它操作的对象的数据类型存在差异,那么您必须将子程序编写为泛型单元,并为每个数据类型参数组合实例化它一次。然后可以将单元的实例化作为常规子程序调用。

您可以通过声明访问子程序值或泛型形式子程序参数来将子程序作为实际参数传递。有关权衡的讨论,请参见指南 5.3.4。

如果您发现自己编写了两个非常相似的例程,它们只在它们操作的数据类型或它们调用的子程序方面有所不同,那么最好将例程编写一次作为泛型单元,并实例化两次以获得所需的两个版本。当以后需要修改这两个例程时,只需要在一个地方进行更改。这极大地简化了维护工作。

做出这种选择后,请考虑这两个实例可能共有的其他方面,这些方面不是例程本质所必需的。将它们作为泛型形式参数分解出来。当以后需要第三个类似的例程时,如果您已经预见到了它与另外两个例程之间的所有差异,那么可以通过第三次实例化自动生成它。参数化的泛型单元可以非常可重用。

编写泛型单元而不是非泛型单元所付出的努力似乎很大。然而,一旦您熟悉泛型设施,编写泛型单元并不比编写非泛型单元困难或耗时得多。在很大程度上,这只是一个练习问题。此外,只要该单元被重用,在单元开发中付出的任何努力都将得到收回,如果该单元被放置在具有足够可见性的重用库中,它肯定会被重用。不要将您对潜在重用的思考局限于您正在进行的应用程序或您非常熟悉的其他应用程序。您不熟悉或将来可能会重用您的软件的应用程序可能会重用您的软件。

编写泛型单元并将其放置在您的重用库中后,您可能要做的第一件事就是根据您的特定需求实例化它一次。此时,最好考虑一下哪些实例很可能被广泛使用。如果是这样,将每个这样的实例放置在您的重用库中,以便其他人可以找到和共享它们。

另请参见指南 9.3.5。

形式私有和受限私有类型

[编辑 | 编辑源代码]
  • 当您在泛型体内部不需要对该类型对象的赋值时,请考虑使用受限私有类型作为泛型形式类型。
  • 当您在泛型体内部需要对该类型对象的正常赋值时,请考虑使用非受限私有类型作为泛型形式类型。
  • 当您需要在泛型体内部对该类型对象强制执行特殊赋值语义时,请考虑使用从 Ada.Finalization.Controlled 派生的形式标记类型。
  • 导出限制最少的类型,以维护数据和抽象的完整性,同时允许替代实现。
  • 请考虑使用受限私有抽象类型作为扩展形式私有标记类型的泛型的泛型形式类型。

第一个示例显示了一种仅提供数据结构的模板情况,在这种情况下,在泛型体内部显然不需要赋值。

------------------------------------------------------------------------
generic
   type Element_Type is limited private;
package Generic_Doubly_Linked_Lists is
   type Cell_Type;
   type List_Type is access all Element_Type;
   type Cell_Type is
      record
         Data     : Element_Type;
         Next     : List_Type;
         Previous : List_Type;
      end record;
end Generic_Doubly_Linked_Lists;

第二个示例显示了一个模板,它从作为泛型形式参数传递的(非赋值)操作中组合新操作。

generic
   type Element_Type is limited private;
   with procedure Process_Element (X : in out Element_Type);
   type List_Type is array (Positive range <>) of Element_Type;
procedure Process_List (L : in out List_Type);
procedure Process_List (L : in out List_Type) is
begin -- Process_List
   for I in L'Range loop
      Process_Element (L(I));
   end loop;
end Process_List;
------------------------------------------------------------------------
generic
   type Domain_Type is limited private;
   type Intermediate_Type is limited private;
   type Range_Type is limited private;
   with function Left (X : Intermediate_Type) return Range_Type;
   with function Right (X : Domain_Type) return Intermediate_Type;
function Generic_Composition (X : Domain_Type) return Range_Type;
-- the function Left o Right
function Generic_Composition (X : Domain_Type) return Range_Type is
begin  -- generic_Composition
   return Left (Right (X));
end Generic_Composition;

第三个示例显示了如何使用 Ada 的受控类型来提供特殊的赋值语义。

with Ada.Finalization;
generic
   type Any_Element is new Ada.Finalization.Controlled with private;
   Maximum_Stack_Size : in Natural := 100;
package Bounded_Stack is
   type Stack is private;
   procedure Push (On_Top      : in out Stack;
                   New_Element : in     Any_Element);
   procedure Pop  (From_Top    : in out Stack;
                   Top_Element :    out Any_Element);
   Overflow  : exception;
   Underflow : exception;
   ...
private
   type Stack_Information;
   type Stack is access Stack_Information;
end Bounded_Stack;

基本原理

[编辑 | 编辑源代码]

为了使泛型组件在尽可能多的上下文中可用,它应该尽量减少对环境的假设,并明确说明任何必要的假设。在 Ada 中,泛型单元做出的假设可以通过泛型形式参数的类型明确说明。受限私有泛型形式类型阻止泛型单元对该类型对象的结构或为该类型对象定义的操作做出任何假设。私有(非受限)泛型形式类型允许假设为该类型定义了赋值和相等比较操作。因此,受限私有数据类型不能指定为私有泛型形式类型的实际参数。

通常,您应该根据泛型内部对赋值的需要选择私有或受限私有泛型形式类型。受限私有类型应该用于不需要赋值的抽象,如上面前两个示例所示。在第三个示例中,需要赋值,指定从受控类型派生的类型以确保正确的赋值语义可用。如果您在泛型体内部需要相等性,您可能还需要重新定义相等性以获得正确的语义;然后,您需要为 = 函数包括形式泛型子程序参数。

可重用部分导出的类型的情况正好相反。对于导出的类型,受限和受限私有指定的限制是针对部分用户的限制,而不是针对部分本身的限制。为了为可重用部分的用户提供最大能力,请导出限制最少的类型。根据需要应用限制,以保护导出数据结构和抽象的完整性,以满足为该泛型设想的各种实现。

由于受限私有类型限制性强,因此它们并不总是可重用部件导出类型的最佳选择。在允许用户复制和比较数据对象以及底层数据类型不涉及访问类型(以便整个数据结构被复制或比较)的情况下,最好导出一个(非受限)私有类型。在允许用户复制和比较数据对象以及底层数据类型涉及访问类型(以便整个数据结构被复制或比较)的情况下,最好导出一个受控类型和一个(重写的)相等运算。在不会损害抽象的情况下,可以使用非私有类型(例如,数值型、枚举型、记录型或数组型)。

泛型单元的一种用途是创建混合泛型(参见指南 8.3.8)来扩展标记类型。在这种情况下,您希望使用最严格的类型作为泛型形式类型,即既受限又抽象的形式类型。当您实例化泛型时,如果实际类型是非受限的,则类型扩展也将是非受限的。在泛型包中,您必须将类型扩展声明为抽象的。然后,泛型的实例化器可以再次扩展该类型以实现所需的混合配置。

注释

[edit | edit source]

预定义包 Sequential_IO 和 Direct_IO 使用私有类型。这会使受限私有类型的 I/O 要求复杂化,应在设计期间加以考虑。

还有一些情况必须使用受限私有形式类型。这些情况出现在形式类型具有访问区分量,或者形式类型用作定义类型扩展的父类型,而该类型扩展本身包含一个受限类型的组件(例如,任务类型),或者形式类型定义了一个新的具有访问区分量的区分量部分。

使用泛型单元封装算法

[edit | edit source]

指南

[edit | edit source]
  • 使用泛型单元封装独立于数据类型的算法。

示例

[edit | edit source]

这是泛型排序过程的规范

------------------------------------------------------------------------
generic
   type Element is private;
   type Data    is array (Positive range <>) of Element;
   with function Should_Precede (Left  : in     Element;
                                 Right : in     Element)
          return Boolean is <>;
 with procedure Swap (Left  : in out Element;
                        Right : in out Element) is <>;
procedure Generic_Sort (Data_To_Sort : in out Data);
------------------------------------------------------------------------

泛型主体看起来就像一个常规过程主体,并且可以充分利用泛型形式参数来实现排序算法

------------------------------------------------------------------------
procedure Generic_Sort (Data_To_Sort : in out Data) is
begin
   ...
   for I in Data_To_Sort'Range loop
      ...
         ...
         if Should_Precede (Data_To_Sort(J), Data_To_Sort(I)) then
            Swap(Data_To_Sort(I), Data_To_Sort(J));
         end if;
         ...
      ...
   end loop;
   ...
end Generic_Sort;
------------------------------------------------------------------------

泛型过程可以实例化为

   type Integer_Array is array (Positive range <>) of Integer;
   function Should_Precede (Left  : in     Integer;
                            Right : in     Integer)
     return Boolean;

   procedure Swap (Left  : in out Integer;
                   Right : in out Integer);
   procedure Sort is
      new Generic_Sort (Element => Integer,
                        Data    => Integer_Array);

或者

   subtype String_80    is String (1 .. 80);
   type    String_Array is array (Positive range <>) of String_80;
   function Should_Precede (Left  : in     String_80;
                            Right : in     String_80)
     return Boolean;

   procedure Swap (Left  : in out String_80;
                   Right : in out String_80);

   procedure Sort is
      new Generic_Sort (Element => String_80,
                        Data    => String_Array);

并调用为

   Integer_Array_1 : Integer_Array (1 .. 100);
   ...
   Sort (Integer_Array_1);

或者

   String_Array_1  : String_Array  (1 .. 100);
   ...
   Sort (String_Array_1);

基本原理

[edit | edit source]

排序算法可以独立于被排序的数据类型来描述。此泛型过程将 Element 数据类型作为泛型受限私有类型参数,因此它对实际操作对象的类型假设尽可能少。它还将 Data 作为泛型形式参数,以便实例化可以将整个数组传递给它们以进行排序。最后,它明确要求执行排序所需的两个运算符:Should_Precede 和 Swap。排序算法在不引用任何数据类型的情况下被封装。泛型可以实例化以对任何数据类型的数组进行排序。8.3.5 使用泛型单元进行数据抽象

指南

[edit | edit source]
  • 考虑使用抽象数据类型(不要与 Ada 的抽象类型混淆)而不是抽象数据对象。
  • 考虑使用泛型单元来实现独立于其组件数据类型的抽象数据类型。

示例

[edit | edit source]

此示例介绍了一系列不同的技术,可用于生成抽象数据类型和对象。对每种技术的优缺点的讨论将在下面的原理部分进行。第一个是抽象数据对象 (ADO),它可用于封装抽象状态机。它封装了一个整数堆栈

------------------------------------------------------------------------
package Bounded_Stack is
   subtype Element is Integer;
   Maximum_Stack_Size : constant := 100;
   procedure Push (New_Element : in     Element);
   procedure Pop  (Top_Element :    out Element);
   Overflow  : exception;
   Underflow : exception;
   ...
end Bounded_Stack;
------------------------------------------------------------------------

第二个示例是抽象数据类型 (ADT)。它与 ADO 不同,因为它导出了 Stack 类型,允许用户声明任意数量的整数堆栈。由于现在可能存在多个堆栈,因此有必要在对 Push 和 Pop 的调用中指定 Stack 参数

------------------------------------------------------------------------
package Bounded_Stack is
   subtype Element is Integer;
   type    Stack   is limited private;
   Maximum_Stack_Size : constant := 100;
   procedure Push (On_Top      : in out Stack;
                   New_Element : in     Element);
   procedure Pop  (From_Top    : in out Stack;
                   Top_Element :    out Element);
   Overflow  : exception;
   Underflow : exception;
   ...
private
   type Stack_Information;
   type Stack is access Stack_Information;
end Bounded_Stack;
------------------------------------------------------------------------

第三个示例是无参数泛型抽象数据对象 (GADO)。它与 ADO(第一个示例)的不同之处仅在于它是泛型的,因此用户可以多次实例化它以获得多个整数堆栈

------------------------------------------------------------------------
generic
package Bounded_Stack is
   subtype Element is Integer;
   Maximum_Stack_Size : constant := 100;
   procedure Push (New_Element : in     Element);
   procedure Pop  (Top_Element :    out Element);
   Overflow  : exception;
   Underflow : exception;
   ...
end Bounded_Stack;
------------------------------------------------------------------------

第四个示例是对第三个示例的略微变体,仍然是 GADO,但具有参数。它与第三个示例的不同之处在于,它将堆栈的数据类型设置为泛型参数,以便可以创建除 Integer 以外其他数据类型的堆栈。此外,Maximum_Stack_Size 已被设置为一个泛型参数,该参数默认为 100,但可以由用户指定,而不是由包定义的常量

------------------------------------------------------------------------
generic
   type Element is private;
   Maximum_Stack_Size : in Natural := 100;
package Bounded_Stack is
   procedure Push (New_Element : in     Element);
   procedure Pop  (Top_Element :    out Element);
   Overflow  : exception;
   Underflow : exception;
   ...
end Bounded_Stack;
------------------------------------------------------------------------

第五个示例是泛型抽象数据类型 (GADT)。它与第四个示例中的 GADO 的不同之处与第二个示例中的 ADT 与第一个示例中的 ADO 的不同之处相同;它导出了 Stack 类型,允许用户声明任意数量的堆栈

------------------------------------------------------------------------
generic
   type Element is private;
   Maximum_Stack_Size : in Natural := 100;
package Bounded_Stack is
   type Stack is private;
   procedure Push (On_Top      : in out Stack;
                   New_Element : in     Element);
   procedure Pop  (From_Top    : in out Stack;
                   Top_Element :    out Element);
   Overflow  : exception;
   Underflow : exception;
   ...
private
   type Stack_Information;
   type Stack is access Stack_Information;
end Bounded_Stack;
------------------------------------------------------------------------

基本原理

[edit | edit source]

ADT 相对于 ADO(或 GADT 相对于 GADO)的最大优势在于,包的用户可以使用 ADT 声明任意数量的对象。这些对象可以声明为独立变量,也可以声明为数组和记录的组件。它们还可以作为参数传递。所有这些对于 ADO 都是不可能的,在 ADO 中,单个数据对象被封装在包中。此外,ADO 不会比 ADT 提供更多的数据结构保护。当私有类型由 ADT 包导出时,如上面的示例所示,那么对于 ADO 和 ADT,唯一可以修改数据的合法操作是包明确定义的操作(在本例中为 Push 和 Pop)。由于这些原因,ADT 或 GADT 几乎总是优于 ADO 或 GADO。

GADO 在一个方面类似于 ADT:它允许用户创建多个对象。使用 ADT,可以使用 ADT 包定义的类型声明多个对象。使用 GADO(即使是没有任何泛型形式参数的 GADO,如第三个示例所示),可以多次实例化包以生成多个对象。但是,相似之处到此为止。实例化生成的多个对象受到上面针对 ADO 描述的所有限制;它们不能用于数组或记录中,也不能作为参数传递。此外,每个对象都是不同类型,并且没有定义对多个对象同时执行的操作。例如,不能执行比较两个对象的运算或将一个对象分配给另一个对象。使用 ADT 包定义的类型声明的多个对象不受任何此类限制;它们可以用于数组和记录中,也可以作为参数传递。此外,它们都被声明为同一类型,因此 ADT 包可以提供操作来分配、比较、复制等。由于这些原因,ADT 几乎总是优于无参数 GADO。

GADT 或 GADO 相对于 ADT 或 ADO 的最大优势在于,GADT 和 GADO 是泛型的,因此可以使用类型、子程序和其他配置信息对其进行参数化。因此,如上所示,一个泛型包可以支持任何数据类型和任何堆栈大小的有限堆栈,而上面的 ADT 和 ADO 则限于 Integer 堆栈,最多 100 个。出于这个原因,GADO 或 GADT 几乎总是优于 ADO 或 ADT。

上面的示例列表按功能和灵活性的顺序排列,从 ADO 开始,以 GADT 结束。这些优势在复杂度或开发时间方面并不昂贵。上面 GADT 的规范与 ADO 的规范在编写或理解方面并没有显著差异。主体也几乎相同。

比较最简单版本 ADO 的主体

package body Bounded_Stack is
   type Stack_Slots is array (Natural range <>) of Element;
   type Stack_Information is
      record
         Slots : Stack_Slots (1 .. Maximum_Stack_Size);
         Index : Natural := 0;
      end record;
   Stack : Stack_Information;
   ---------------------------------------------------------------------
   procedure Push (New_Element : in     Element) is
   begin
      if Stack.Index >= Maximum_Stack_Size then
         raise Overflow;
      end if;
      Stack.Index := Stack.Index + 1;
      Stack.Slots(Stack.Index) := New_Element;
   end Push;
   ---------------------------------------------------------------------
   procedure Pop (Top_Element :    out Element) is
   begin
      if Stack.Index <= 0 then
         raise Underflow;
      end if;
      Top_Element := Stack.Slots(Stack.Index);
      Stack.Index := Stack.Index - 1;
   end Pop;
   ---------------------------------------------------------------------
   ...
end Bounded_Stack;

与功能最强大、最灵活的版本 GADT 的主体

package body Bounded_Stack is
   type Stack_Slots is array (Natural range <>) of Element;
   type Stack_Information is
      record
         Slots : Stack_Slots (1 .. Maximum_Stack_Size);
         Index : Natural := 0;
      end record;
   ---------------------------------------------------------------------
   procedure Push (On_Top      : in out Stack;
                   New_Element : in     Element) is
   begin
      if On_Top.Index >= Maximum_Stack_Size then
         raise Overflow;
      end if;
      On_Top.Index := On_Top.Index + 1;
      On_Top.Slots(On_Top.Index) := New_Element;
   end Push;
   ---------------------------------------------------------------------
   procedure Pop (From_Top    : in out Stack;
                  Top_Element :    out Element) is
   begin
      if From_Top.Index <= 0 then
         raise Underflow;
      end if;
      Top_Element := From_Top.Slots(From_Top.Index);

      From_Top.Index := From_Top.Index - 1;
   end Pop;
   ---------------------------------------------------------------------
   ...
end Bounded_Stack;

只有一点不同。ADO 声明了一个名为 Stack 的局部对象,而 GADT 在每个导出的过程 Push 和 Pop 上都有一个额外的参数(名为 Stack)。

迭代器

[edit | edit source]
  • 为可重用部分中复杂数据结构的遍历提供迭代器。
  • 考虑提供主动和被动迭代器。
  • 保护迭代器免受迭代期间数据结构修改导致的错误。
  • 记录迭代器在遍历期间数据结构被修改时的行为。

Ada 提供了多种机制来构建可重用迭代器。以下示例讨论了“简单”泛型、访问辨别式和类型扩展的替代方案。术语主动和被动用于区分迭代机制(即遍历复杂数据结构的方式)是公开还是隐藏。被动迭代器隐藏了遍历(例如循环机制),并且包含单个操作 iterate,该操作以您对数据结构中每个元素执行的处理进行参数化。相反,主动迭代器公开了用于遍历数据结构的基本操作(Booch 1987)。

第一个示例展示了一个泛型包,它定义了一个抽象列表数据类型,以及用于遍历列表的主动和被动迭代器。

------------------------------------------------------------------------
generic
   type Element is limited private;
   ...
package Unbounded_List is
   type List is limited private;
   procedure Insert (New_Element : in     Element;
                     Into        : in out List);
   -- Passive (generic) iterator.
   generic
      with procedure Process (Each : in out Element);
   procedure Iterate (Over : in     List);
   -- Active iterator
   type Iterator is limited private;

   procedure Initialize (Index         : in out Iterator;
                         Existing_List : in     List);

   function  More       (Index         : in     Iterator)
     return Boolean;

   -- The procedure Get_Next combines an "Advance" and "Current" function
   procedure Get_Next   (Index           : in out Iterator;
                         Current_Element :    out Element);
   ...
private
   ...
end Unbounded_List;
------------------------------------------------------------------------

在实例化泛型包并声明一个列表为

------------------------------------------------------------------------
with Unbounded_List;
procedure List_User is
   type Employee is ...;
   package Roster is
      new Unbounded_List (Element => Employee, ...);
   Employee_List : Roster.List;

被动迭代器被实例化,指定了在调用迭代器时应为每个列表元素调用的例程的名称。

   ---------------------------------------------------------------------
   procedure Process_Employee (Each : in out Employee) is
   begin
      ...
      -- Perform the required action for EMPLOYEE here.
   end Process_Employee;
   ---------------------------------------------------------------------
   procedure Process_All is
      new Roster.Iterate (Process => Process_Employee);

然后可以将被动迭代器调用为

begin  -- List_User
   Process_All (Employee_List);
end List_User;
------------------------------------------------------------------------

或者,可以使用主动迭代器,而无需被动迭代器所需的第二个实例化

   Iterator         : Roster.Iterator;
   Current_Employee : Employee;
   procedure Process_Employee (Each : in     Employee) is separate;
begin  -- List_User
   Roster.Initialize (Index         => Iterator,
                      Existing_List => Employee_List);

   while Roster.More (Iterator) loop

      Roster.Get_Next (Index           => Iterator,
                       Current_Element => Current_Employee);

      Process_Employee (Current_Employee);

   end loop;
end List_User;
------------------------------------------------------------------------

第二个示例展示了《Rationale》(1995,§3.7.1)中关于如何使用访问辨别式构建迭代器的代码片段。

generic
   type Element is private;
package Sets is
   type Set is limited private;
   ... -- various set operations
   type Iterator (S : access Set) is limited private;
   procedure Start (I : Iterator);
   function Done (I : Iterator) return Boolean;
   procedure Next (I : in out Iterator);
   ...  -- other iterator operations
private
   type Node;
   type Ptr is access Node;
   type Node is
      record
         E    : Element;
         Next : Ptr;
      end record;
   type Set is new Ptr;
   type Iterator (S : access Set) is
      record
         This : Ptr;
      end record;
end Sets;
package body Sets is
   ...  -- bodies of the various set operations
   procedure Start (I : in out Iterator) is
   begin
      I.This := Ptr(I.S.all);
   end Start;
   function Done (I : Iterator) return Boolean is
   begin
      return I.This = null;
   end Done;
   procedure Next (I : in out Iterator) is
   begin
      I.This := I.This.Next;
   end Next;
   ...
end Sets;

迭代器操作允许您迭代集合的元素,其中迭代器对象的组件 This 访问当前元素。访问辨别式始终指向当前元素所属的封闭集合。

第三个示例使用《Rationale》(1995,§4.4.4)中的代码片段来展示使用类型扩展和分派的迭代器。

type Element is ...
package Sets is
   type Set is limited private;
   -- various set operations
   type Iterator is abstract tagged null record;
   procedure Iterate (S : in Set; IC : in out Iterator'Class);
   procedure Action (E : in out Element;
                     I : in out Iterator) is abstract;
private
   -- definition of Node, Ptr (to Node), and Set
end Sets;
package body Sets is
   ...
   procedure Iterate (S : in Set; IC : in out Iterator'Class) is
      This : Ptr := Ptr (S);
   begin
      while This /= null loop
         Action (This.E, IC);  -- dispatch
         This := This.Next;
      end loop;
   end Iterate;
end Sets;

通用迭代器如下所示

package Sets.Something is
   procedure Do_Something (S : Set; P : Parameters);
end Sets.Something;
package body Sets.Something is
   type My_Iterator is new Iterator with
      record
         -- components for parameters and workspace
      end record;
   procedure Action (E : in out Element;
                     I : in out My_Iterator) is
   begin
      -- do something to element E using data from iterator I
   end Action;
   procedure Do_Something (S : Set; P : Parameters) is
      I : My_Iterator;
   begin  -- Do_Something
      ...  -- copy parameters into iterator
      Iterate (S, I);
      ... copy any results from iterator back to parameters
   end Do_Something;

end Sets.Something;

基本原理

[编辑 | 编辑源代码]

经常需要遍历复杂数据结构,如果部分本身没有提供,则在不违反信息隐藏原则的情况下很难实现。

主动和被动迭代器各有优缺点,但并非在所有情况下都适用。因此,建议提供这两种类型的迭代器,让用户可以选择在每种情况下使用哪种迭代器。

被动迭代器比主动迭代器更简单,错误率也更低,就像 for 循环比 while 循环更简单,错误率也更低。用户使用被动迭代器时会犯更少的错误。只需使用要为每个列表元素执行的例程对其进行实例化,然后为所需的列表调用实例化。主动迭代器要求用户更加谨慎。必须谨慎地按正确顺序调用迭代器操作,并将正确的迭代器变量与正确的列表变量相关联。在维护过程中对软件进行的更改可能会引入错误,例如无限循环。

另一方面,主动迭代器比被动迭代器更灵活。使用被动迭代器,很难执行多个并发同步迭代。例如,使用主动迭代器遍历两个排序列表并将它们合并到第三个排序列表中要容易得多。此外,对于多维数据结构,少量主动迭代器例程可以替换大量被动迭代器,每个被动迭代器实现主动迭代器的一种组合。最后,主动迭代器可以作为泛型形式参数传递,而被动迭代器则不能,因为被动迭代器本身就是泛型的,泛型单元不能作为参数传递给其他泛型单元。

对于任何类型的迭代器,关于在迭代数据结构时发生什么的语义问题都会出现。在编写迭代器时,一定要考虑这种可能性,并在注释中指明在这种情况下发生的行为。对于用户来说,这并不总是显而易见。例如,为了确定某个操作相对于某个数学“集合”的“闭包”,一个常见的算法是对集合的成员进行迭代,生成新元素并将它们添加到集合中。在这种情况下,重要的是在迭代过程中添加到集合中的元素在随后的迭代过程中被遇到。另一方面,对于其他算法,重要的是迭代的集合与迭代开始时存在的集合相同。在优先级列表数据结构的情况下,如果以优先级顺序迭代列表,则重要的是在迭代过程中插入的优先级低于当前元素的元素在随后的迭代过程中不会被遇到,但优先级更高的元素应该被遇到。在任何情况下,都要有意识地决定迭代器应该如何运行,并在包规范中记录该行为。

从数据结构中删除元素也会给迭代器带来问题。用户经常会犯的一个错误是迭代一个数据结构,并在迭代期间逐个删除它。如果迭代器没有为这种情况做好准备,则可能最终会取消引用空指针或犯类似的错误。可以通过在每个数据结构中存储额外信息来防止这种情况,这些信息指示它是否当前正在被迭代,并使用此信息来禁止在迭代期间对数据结构进行任何修改。当数据结构被声明为有限私有类型时,就像在涉及迭代器时通常应该做的那样,对该类型定义的唯一操作是在声明该类型的包中明确声明,这使得可以将这些测试添加到所有修改操作中。

《Rationale》(1995,§4.4.4)指出,访问辨别式和类型扩展技术是彼此的逆运算。在访问辨别式方法中,您必须为每个操作编写出循环机制。在类型扩展方法中,您编写一个循环并分派到所需的动作。因此,使用访问辨别式技术的迭代器将被视为主动的,而使用类型扩展技术的迭代器将被视为被动的。

您可以使用对子程序类型的访问作为泛型实例化的替代方法,使用非泛型参数作为指向子程序的指针。然后,您将引用子程序应用于集合中的每个元素(《Rationale》1995,§3.7.2)。但是,这种方法有一些缺点,因为您不能使用它来创建通用迭代器。匿名访问子程序参数在 Ada 中不允许;因此,以下片段是非法的

procedure Iterate (C      : Collection;
                   Action : access procedure (E : in out Element));

形式参数 Action 必须是命名访问子类型的,例如

type Action_Type is access procedure (E : in out Element);
procedure Iterate (C      : Collection;
                   Action : Action_Type);

为了使此方法有效,您必须确保操作子程序在作用域内,并且未在另一个子程序内部定义。如果它是作为嵌套过程定义的,则访问它将是非法的。请参阅《Rationale》(1995,§4.4.4)以获取更完整的示例。

有关被动和主动迭代器的进一步讨论,请参阅《Rationale》(1995,§3.7.1 和 §4.4.4)、Ross(1989)和 Booch(1987)。

十进制类型输出和信息系统附录

[编辑 | 编辑源代码]
  • 在图片输出中本地化货币符号、数字分隔符、基数标记和填充字符。
  • 考虑在图片布局中使用 # 字符,以便编辑后的数字输出长度在不同长度的货币符号之间保持不变。
with Ada.Text_IO.Editing;
package Currency is

   type Dollars is delta 0.01 digits 10;
   type Marks   is delta 0.01 digits 10;

   package Dollar_Output is
      new Ada.Text_IO.Editing.Decimal_Output
             (Num                => Dollars,
              Default_Currency   => "$",
              Default_Fill       => '*',
              Default_Separator  => ',',
              Default_Radix_Mark => '.');

   package Mark_Output is
      new Ada.Text_IO.Editing.Decimal_Output
             (Num                => Marks,
              Default_Currency   => "DM",
              Default_Fill       => '*',
              Default_Separator  => '.',
              Default_Radix_Mark => ',');

end Currency;
with Ada.Text_IO.Editing;
with Currency;  use Currency;
procedure Test_Picture_Editing is

   DM_Amount     : Marks;
   Dollar_Amount : Dollars;

   Amount_Picture : constant Ada.Text_IO.Editing.Picture 
      := Ada.Text_IO.Editing.To_Picture ("##ZZ_ZZZ_ZZ9.99");

begin   -- Test_Picture_Editing

   DM_Amount     := 1_234_567.89;
   Dollar_Amount := 1_234_567.89;

   DM_Output.Put (Item => DM_Amount,
                  Pic  => Amount_Picture);

   Dollar_Output.Put (Item => Dollar_Amount,
                      Pic  => Amount_Picture);
   
end Test_Picture_Editing;

基本原理

[编辑 | 编辑源代码]

货币在报表中的显示方式有所不同。货币使用不同长度的不同符号(例如,美国 $、德国 DM 和奥地利 ÖS)。它们使用不同的符号来分隔数字。美国和英国使用逗号来分隔千位数,而欧洲大陆使用句号。美国和英国使用句号作为小数点;欧洲大陆使用逗号。对于涉及跨国使用的财务计算的程序,您需要考虑这些差异。通过封装它们,您可以限制在调整财务包时更改的影响。

实现 Mixin

[编辑 | 编辑源代码]
  • 考虑使用抽象标记类型和泛型来定义可重用的功能单元,这些单元可以“混合”到核心抽象中(也称为 Mixin)。

注意以下模式中使用抽象标记类型作为泛型形式参数以及作为导出扩展类型,该模式摘自《Rationale》(1995,§4.6.2)。

generic
   type S is abstract tagged private;
package P is
   type T is abstract new S with private;
   -- operations on T
private
   type T is abstract new S with
      record
         -- additional components
      end record;
end P;

以下代码展示了如何实例化泛型以在最终类型扩展中“混合”所需的功能。另请参阅准则 9.5.1,了解相关的代码示例。

-- Assume that packages P1, P2, P3, and P4 are generic packages which take a tagged
-- type as generic formal type parameter and which export a tagged type T
package Q is
   type My_T is new Basic_T with private;
   ... -- exported operations
private
   package Feature_1 is new P1 (Basic_T);
   package Feature_2 is new P2 (Feature_1.T);
   package Feature 3 is new P3 (Feature_2.T);
   package Feature_4 is new P4 (Feature_3.T);
   -- etc.
   type My_T is new Feature_4.T with null record;
end Q;

基本原理

[edit | edit source]

《原理》(1995 年,第 4.6.2 节)讨论了使用泛型模板来定义要混合到抽象中的属性。

泛型模板定义了混入。作为泛型实参提供的类型决定了父类...主体提供了操作,规范导出了扩展类型。

如果你定义了一系列泛型混入包,你就可以对实例化进行序列化。下一个实例化的实际参数是前一个实例化导出的带标签类型。这在示例中的第二个代码段中显示。每个扩展都源自前一个扩展,因此你有一个线性化的子程序覆盖序列。由于它们是线性化的,所以你有一个可以用来解决任何冲突的派生顺序。

你应该将一个扩展(以及相关操作)封装到每个泛型包中。这提供了更好的关注点分离,以及更可维护、可重用的组件。

有关混入使用的完整讨论,请参见指南 9.5.1。

独立性

[edit | edit source]

可重用部分应该尽可能独立于其他可重用部分。如果一个潜在的用户需要使用其他看起来不必要的部件才能重用一个部件,那么他重用该部件的可能性就会降低。其他部件的“额外负担”会浪费时间和空间。用户希望能够只重用被认为有用的部分。“部分”的概念在这里故意含糊不清。如果来自该库的通常被重用的“部分”是整个子系统,那么单个包不需要独立于重用库中的每个其他包。如果整个子系统被认为是提供有用的功能,那么整个子系统就被重用。但是,子系统不应该与重用库中的所有其他子系统紧密耦合,以至于难以或不可能在不重用整个库的情况下重用子系统。可重用部分之间的耦合只有在它为用户提供了明显的强大优势时才会发生。

子系统设计

[edit | edit source]

指南

[edit | edit source]
  • 考虑将子系统结构化,以便仅在特定上下文中使用的操作位于与在不同上下文中使用的操作不同的子包中。
  • 考虑在父包中声明与上下文无关的功能,在子包中声明与上下文相关的功能。

基本原理

[edit | edit source]

泛型单元是基本的构建块。泛型参数化可用于打破程序单元之间的依赖关系,以便它们可以单独重用。但是,通常情况下,一组单元(尤其是包集)将作为子系统一起重用。在这种情况下,这些包可以被收集到一个子包层次结构中,并使用私有包隐藏内部细节。层次结构可能是泛型的,也可能不是泛型的。使用子包允许在不包含太多无关操作的情况下重用子系统,因为未使用的子包可以在新环境中被丢弃。

另请参见指南 4.1.6 和 8.3.1。

使用泛型参数减少耦合

[edit | edit source]

指南

[edit | edit source]
  • 最小化可重用部件上的 with 子句,尤其是在它们的规范上。
  • 考虑使用泛型参数而不是 with 语句来减少可重用部件上的上下文子句数量。
  • 考虑使用泛型形式包参数直接导入预先存在的泛型实例中定义的所有类型和操作。

示例

[edit | edit source]

像下面的过程

------------------------------------------------------------------------
with Package_A;
procedure Produce_And_Store_A is
   ...
begin  -- Produce_And_Store_A
   ...
   Package_A.Produce (...);
   ...
   Package_A.Store (...);
   ...
end Produce_And_Store_A;
------------------------------------------------------------------------

可以重写为一个泛型单元

------------------------------------------------------------------------
generic
   with procedure Produce (...);
   with procedure Store   (...);
procedure Produce_And_Store;
------------------------------------------------------------------------
procedure Produce_And_Store is
   ...
begin  -- Produce_And_Store
   ...
   Produce (...);
   ...
   Store   (...);
   ...
end Produce_And_Store;
------------------------------------------------------------------------

然后实例化

------------------------------------------------------------------------
with Package_A;
with Produce_And_Store;
procedure Produce_And_Store_A is
   new Produce_And_Store (Produce => Package_A.Produce,
                          Store   => Package_A.Store);
------------------------------------------------------------------------

基本原理

[edit | edit source]

上下文(with)子句指定了该单元所依赖的其他单元的名称。这种依赖关系不能也不应该完全避免,但尽量减少出现在单元规范中的依赖关系数量是个好主意。尝试将它们移到主体中,使规范独立于其他单元,以便更容易地单独理解。此外,以这样的方式组织你的可重用部分,即单元的主体中不包含大量相互依赖关系。将你的库划分为独立的功能区域,且没有跨区域的依赖关系,是一个良好的开端。最后,通过使用泛型形式参数而不是 with 语句,如上面的示例所示,来减少依赖关系。如果库中的单元耦合得太紧密,那么任何单个部分都无法在不重用大部分或全部库的情况下被重用。

上面的 Produce_And_Store_A 的第一个(非泛型)版本很难重用,因为它依赖于 Package_A,而 Package_A 可能不是通用的或普遍可用的。如果 Produce_And_Store 操作的重用潜力因这种依赖关系而降低,则应该生成一个泛型单元和一个实例化,如上所示。Package_A 的 with 子句已从 Produce_And_Store 泛型过程(它封装了可重用算法)中移到了 Produce_And_Store_A 实例化中。泛型单元不再命名提供所需操作的包,而是简单地列出所需的操作本身。这增加了泛型单元的独立性和可重用性。

这种将泛型形式参数用于 with 子句的方法也允许更细粒度的可见性。非泛型版本的 Produce_And_Store_A 上的 with 子句使 Package_A 的所有内容对 Produce_And_Store_A 可见,而泛型版本上的泛型参数仅使 Produce 和 Store 操作对泛型实例化可用。

泛型形式包允许“更安全、更简单的泛型抽象组合”(原理 1995 年,第 12.6 节)。泛型形式包允许你将一组相关的类型及其操作分组到一个单元中,避免将每个类型和操作作为单独的泛型形式参数列出。这种技术允许你清楚地表明你正在用另一个泛型扩展一个泛型的功能,实际上是用另一个抽象参数化一个抽象。

由于编译指示造成的耦合

[edit | edit source]

指南

[edit | edit source]
  • 在泛型库单元的规范中,使用 pragma Elaborate_Body。

示例

[edit | edit source]
---------------------------------------------------------------------------
generic
   ...
package Stack is

   pragma Elaborate_Body (Stack); -- in case the body is not yet elaborated

   ...
end Stack;
---------------------------------------------------------------------------
with Stack;
package My_Stack is
   new Stack (...);
---------------------------------------------------------------------------
package body Stack is
begin
   ...
end Stack;
---------------------------------------------------------------------------

基本原理

[edit | edit source]

编译单元的细化顺序仅受限于编译顺序。此外,无论何时你有一个作为库单元的实例化或一个在库包中的实例化,Ada 都要求你在细化实例化本身之前细化正在被实例化的泛型的主体。由于泛型库单元主体可能在泛型的实例化之后编译,因此该主体可能不会在实例化时被细化,从而导致 Program_Error。使用 pragma Elaborate_Body 通过要求泛型单元主体在规范之后立即细化来避免这种情况,无论编译顺序如何。

当存在明确的递归依赖关系要求时,应使用 pragma Elaborate_Body。这种情况出现在例如,当存在递归依赖关系(例如,包 A 的主体依赖于包 B 的规范,而包 B 的主体依赖于包 A 的规范)时。

Pragma Elaborate_All 控制一个单元相对于另一个单元的细化顺序。这是另一种耦合单元的方式,在可重用部分中应尽可能避免,因为它限制了可重用部分可以组合的配置数量。但是请认识到,pragma Elaborate_All 提供了更好的细化顺序保证,因为如果使用此 pragma 发现细化问题,这些问题将在链接时报告(而不是在运行时执行错误)。

在库单元的细化过程中调用子程序(通常是函数)时,子程序的主体必须在库单元之前细化。可以通过为包含函数的单元添加 pragma Elaborate_Body 来确保此细化发生。但是,如果该函数调用其他函数,那么在包含该函数的单元上放置 pragma Elaborate_All 则更安全。

有关 pragma Pure 和 Preelaborate 的讨论,另请参见 Ada 参考手册(1995,§10.2.1 [注释]) 和理由(1995,§10.3)。如果使用 pragma Pure 或 Preelaborate,则不需要 pragma Elaborate_Body。

注册的概念是许多面向对象编程框架的基础。由于其他库单元需要在细化过程中调用它,因此需要确保注册本身尽早细化。请注意,注册表只应依赖于类型层次结构的根类型,并且注册表只应保存指向对象的“类级”指针,而不是更具体的指针。根类型本身不应依赖于注册表。有关面向对象特性的更完整讨论,请参见第 9 章。

部分族

[编辑 | 编辑源代码]
  • 创建具有类似规范的泛型或其他部分的族。

Booch 部分(Booch 1987)是应用此指南的示例。

基本原理

[编辑 | 编辑源代码]

对于不同的应用程序,或者为了改变给定应用程序的属性,可能需要类似部分的不同版本(例如,有界栈与无界栈)。通常,这些版本所需的不同的行为无法使用泛型参数获得。提供具有类似规范的部分族使程序员可以轻松地为当前应用程序选择合适的版本,或者在应用程序需求发生变化时替换其他版本。

通过替换族成员,由作为部分族成员的子部分构建的可重用部分特别容易调整以适应给定应用程序的需要。

指南 9.2.4 讨论了在构建类似部分(即通用接口,多个实现)的不同版本时使用标记类型的用法。

条件编译

[编辑 | 编辑源代码]
  • 构建可重用代码以利用编译器进行死代码移除。
------------------------------------------------------------------------
package Matrix_Math is
   ...
   type Algorithm is (Gaussian, Pivoting, Choleski, Tri_Diagonal);
   generic
      Which_Algorithm : in     Algorithm := Gaussian;
   procedure Invert ( ... );
end Matrix_Math;
------------------------------------------------------------------------
package body Matrix_Math is
   ...
   ---------------------------------------------------------------------
   procedure Invert ( ... ) is
      ...
   begin  -- Invert
      case Which_Algorithm is
         when Gaussian     => ... ;
         when Pivoting     => ... ;
         when Choleski     => ... ;
         when Tri_Diagonal => ... ;
      end case;
   end Invert;
   ---------------------------------------------------------------------
end Matrix_Math;
------------------------------------------------------------------------

基本原理

[编辑 | 编辑源代码]

某些编译器会省略与他们检测到的永远不会执行的程序部分相对应的目标代码。条件语句中的常量表达式利用了此功能(如果可用),提供了有限形式的条件编译。当在不支持此形式的条件编译的实现中重用部分时,这种做法会产生一个干净的结构,通过删除或注释掉冗余代码(如果它会产生不可接受的开销)可以轻松地进行调整。

当其他因素阻止代码被分成单独的程序单元时,应该使用此功能。在上面的示例中,最好为每种算法提供不同的过程。但这些算法在细微但复杂的方式上有所不同,使得单独的过程难以维护。

请注意您的实现是否支持死代码移除,并准备好采取其他措施来消除冗余代码的开销(如果有必要)。

表驱动编程

[编辑 | 编辑源代码]
  • 只要可能且适当,编写表驱动的可重用部分。

表驱动的可重用软件的典型代表是解析器生成系统。输入数据形式及其输出的规范,以及一些专用代码,被转换为表格,这些表格将被使用预定算法的预先存在的代码使用,这些算法在生成的解析器中使用。其他形式的“应用程序生成器”的工作原理类似。

基本原理

[编辑 | 编辑源代码]

表驱动(有时称为数据驱动)程序的行为依赖于在编译时 with 的数据或在运行时从文件读取的数据。在适当的情况下,表驱动编程提供了一种非常强大的方法来创建通用的、易于定制的、可重用部分。

有关在实现表驱动程序时使用访问子程序类型的简短讨论,请参见指南 5.3.4。

考虑通用部分的行为差异是否可以通过在编译时或运行时定义的某些数据结构来定义,如果是,则构建该部分使其成为表驱动的。当设计部分用于特定应用程序领域但需要针对该领域中的特定应用程序进行专门化时,这种方法最有可能适用。在评论驱动部分所需数据的结构时要特别注意。

表驱动程序通常比相应的 case 或 if-elsif-else 网络更有效,并且更容易阅读,以计算要查找或查找的项。

字符串处理

[编辑 | 编辑源代码]
  • 使用预定义的包进行字符串处理。

在 Ada 95 中,不再需要编写以下代码

function Upper_Case (S : String) return String is

   subtype Lower_Case_Range is Character range 'a'..'z';

   Temp : String := S;
   Offset : constant := Character'Pos('A') - Character'Pos('a');

begin
   for Index in Temp'Range loop
      if Temp(Index) in Lower_Case_Range then
         Temp(Index) := Character'Val (Character'Pos(Temp(Index)) + Offset);
      end if;
   end loop;
   return Temp;
end Upper_Case;

with Ada.Characters.Latin_1;
function Trim (S : String) return String is
   Left_Index  : Positive := S'First;
   Right_Index : Positive := S'Last;
   Space : constant Character := Ada.Characters.Latin_1.Space;
begin
   while (Left_Index < S'Last) and then (S(Left_Index) = Space) loop
      Left_Index := Positive'Succ(Left_Index);
   end loop;

   while (Right_Index > S'First) and then (S(Right_Index) = Space) loop
      Right_Index := Positive'Pred(Right_Index);
   end loop;

   return S(Left_Index..Right_Index);
end Trim;

假设变量 S 的类型为 String,以下表达式

Upper_Case(Trim(S))

现在可以替换为更便携且预先存在的语言定义的操作,例如

with Ada.Characters.Handling;  use Ada.Characters.Handling;
with Ada.Strings;              use Ada.Strings;
with Ada.Strings.Fixed;        use Ada.Strings.Fixed;

...
To_Upper (Trim (Source => S, Side => Both))

基本原理

[编辑 | 编辑源代码]

预定义的 Ada 语言环境包含字符串处理包,以鼓励可移植性。它们支持不同类别的字符串:固定长度、有界长度和无界长度。它们还支持用于字符串构造、连接、复制、选择、排序、搜索、模式匹配和字符串转换的子程序。您不再需要定义自己的字符串处理包。

带标记的类型层次结构

[edit | edit source]

指南

[edit | edit source]
  • 考虑使用带标记的类型层次结构来促进软件的泛化,以便重复使用。
  • 考虑使用带标记的类型层次结构将泛化算法与对特定类型依赖关系的细节分离。

示例

[edit | edit source]
with Wage_Info;
package Personnel is
   type Employee is abstract tagged limited private;
   type Employee_Ptr is access all Employee'Class;
   ...
   procedure Compute_Wage (E : Employee) is abstract;
private
   type Employee is tagged limited record
      Name  : ...;
      SSN   : ... ;
      Rates : Wage_Info.Tax_Info;
      ...
   end record;
end Personnel;
package Personnel.Part_Time is
   type Part_Timer is new Employee with private;
   ...
   procedure Compute_Wage (E : Part_Timer);
private
   ...
end Personnel.Part_Time;
package Personnel.Full_Time is
   type Full_Timer is new Employee with private;
   ...
   procedure Compute_Wage (E : Full_Timer);
private
   ...
end Personnel.Full_Time;

给定以下数组声明

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

您可以编写一个计算每个员工工资的程序,无论您创建了多少种不同的员工类型。Employee_List 包含一个指向各种类型员工的指针数组,每个员工都有一个单独的 Compute_Wage 过程。(原始 Compute_Wage 被声明为一个抽象过程,因此必须由所有后代覆盖。)您无需修改工资单代码,因为您专门化了员工类型

procedure Compute_Payroll (Who : Employee_List) is
begin -- Compute_Payroll
   for E in Who'Range loop
      Compute_Wage (Who(E).all);
   end loop;
end Compute_Payroll;

基本原理

[edit | edit source]

一般算法可以多态地依赖于根带标记类型的类宽类型对象,而无需关心从根类型派生的特定类型。如果在类型层次结构中添加了其他类型,则无需更改泛化算法。另请参见准则 5.4.2。此外,子包层次结构将镜像继承层次结构。

一个通用的根带标记类型可以定义层次结构中更特定类型的共同属性并具有共同的操作。仅依赖于此根类型的软件将是通用的,因为它可以与任何更特定类型的对象一起使用。此外,根类型客户机的一般算法不必随着在类型层次结构中添加更多特定类型而更改。这是一种组织面向对象软件以进行重复使用的特别有效的方法。

将派生带标记类型层次结构分离到各个包中,通过减少包接口中的项目数量来增强可重用性。它还允许您只使用所需的 capabilities。

另请参见准则 9.2、9.3.1、9.3.5 和 9.4.1。

摘要

[edit | edit source]

理解和清晰

[edit | edit source]
  • 为可重用部分及其标识符选择限制最少的名称。
  • 选择通用名称以避免与通用实例的命名约定冲突。
  • 使用表示可重用部分行为特征及其抽象的名称。
  • 在标识符或单元名称中不要使用缩写。
  • 像文档化任何包规范一样,文档化泛型形式参数的预期行为。

健壮性

[edit | edit source]
  • 使用命名的数字和静态表达式,可以让多个依赖关系链接到少量符号。
  • 对于数组形式参数和数组返回值,使用无约束数组类型。
  • 在适当的情况下,使局部变量的大小取决于实际参数的大小。
  • 最小化单元做出的假设数量。
  • 对于无法避免的假设,使用子类型或约束来自动强制一致性。
  • 对于无法通过子类型自动强制执行的假设,请在代码中添加显式检查。
  • 记录所有假设。
  • 如果代码依赖于特定特殊需求附件的实现才能正常运行,请在代码中记录此假设。
  • 在声明模式为 in out 的泛型形式对象时,使用第一个子类型。
  • 注意在声明泛型形式子程序的参数或返回值时使用子类型作为子类型标记。
  • 使用属性而不是字面量。
  • 谨慎对待重载同一个泛型包导出的子程序的名称。
  • 在规范内,记录通过使用规范和使用规范的任何部分而激活的任何任务。
  • 记录从泛型单元中隐藏的任务中访问哪些泛型形式参数。
  • 记录任何多线程组件。
  • 将异常从可重用部分传播出去。仅当您确定处理在所有情况下都合适时,才在可重用部分内处理异常。
  • 在执行泛型形式子程序正确操作的任何必要清理后,传播泛型形式子程序引发的异常。
  • 在引发异常时,将状态变量置于有效状态。
  • 在引发异常时,将参数保持不变。

适应性

[edit | edit source]
  • 在可重用部分或一组部分中提供核心功能,以便可以在此抽象级别中对其进行有意义的扩展,使其能够被重复使用。
  • 更具体地说,为每个可能包含动态数据的的数据结构提供初始化和终结过程。
  • 对于需要初始化和终结的数据结构,请考虑尽可能从类型 Ada.Finalization.Controlled 或 Ada.Finalization.Limited_Controlled 派生它们。
  • 使用泛型单元避免代码重复。
  • 为泛型单元参数化以实现最大适应性。
  • 重用泛型单元的常见实例,以及泛型单元本身。
  • 当您在泛型体内部不需要对该类型对象的赋值时,请考虑使用受限私有类型作为泛型形式类型。
  • 当您在泛型体内部需要对该类型对象的正常赋值时,请考虑使用非受限私有类型作为泛型形式类型。
  • 当您需要在泛型体内部对该类型对象强制执行特殊赋值语义时,请考虑使用从 Ada.Finalization.Controlled 派生的形式标记类型。
  • 导出限制最少的类型,以维护数据和抽象的完整性,同时允许替代实现。
  • 请考虑使用受限私有抽象类型作为扩展形式私有标记类型的泛型的泛型形式类型。
  • 使用泛型单元封装独立于数据类型的算法。
  • 考虑使用抽象数据类型(不要与 Ada 的抽象类型混淆)而不是抽象数据对象。
  • 考虑使用泛型单元来实现独立于其组件数据类型的抽象数据类型。
  • 为可重用部分中复杂数据结构的遍历提供迭代器。
  • 考虑提供主动和被动迭代器。
  • 保护迭代器免受迭代期间数据结构修改导致的错误。
  • 记录迭代器在遍历期间数据结构被修改时的行为。
  • 在图片输出中本地化货币符号、数字分隔符、基数标记和填充字符。
  • 考虑在图片布局中使用 # 字符,以便编辑后的数字输出长度在不同长度的货币符号之间保持不变。
  • 考虑使用抽象标记类型和泛型来定义可重用的功能单元,这些单元可以“混合”到核心抽象中(也称为 Mixin)。
  • 考虑将子系统结构化,以便仅在特定上下文中使用的操作位于与在不同上下文中使用的操作不同的子包中。
  • 考虑在父包中声明与上下文无关的功能,在子包中声明与上下文相关的功能。

独立性

[edit | edit source]
  • 最小化可重用部件上的 with 子句,尤其是在它们的规范上。
  • 考虑使用泛型参数而不是 with 语句来减少可重用部件上的上下文子句数量。
  • 考虑使用泛型形式包参数直接导入预先存在的泛型实例中定义的所有类型和操作。
  • 在泛型库单元的规范中,使用 pragma Elaborate_Body。
  • 创建具有类似规范的泛型或其他部分的族。
  • 构建可重用代码以利用编译器进行死代码移除。
  • 只要可能且适当,编写表驱动的可重用部分。
  • 使用预定义的包进行字符串处理。
  • 考虑使用带标记的类型层次结构来促进软件的泛化,以便重复使用。
  • 考虑使用带标记的类型层次结构将泛化算法与对特定类型依赖关系的细节分离。

面向对象特性

华夏公益教科书