本文档 Rational Unified Process - Ada 编程准则是可用来为您的组织派生出一种编码标准的模板。该指南说明了应如何编写 Ada 程序。本文档的适用对象是所有将 Ada 用作实施语言或者用作设计语言(例如用于指定接口或数据结构)的应用软件设计者和开发者。
本文档中所描述的规则涵盖了代码编写的大部分方面。通用规则适用于程序布局、命名约定和注释使用。特定规则适用于选定的 Ada 特性并说明了禁用构造、推荐使用模式和用来提高程序质量的通用技巧。
项目的设计准则和当前的编程准则有一定程度的重叠,这样做是有目的的。文档中介绍了许多代码编写规则(尤其是在命名约定方面)以积极地支持和加强面向对象的软件设计方法。
准则最初是针对 Ada 83 而编写的。其中含有针对 Ada 95 的兼容性规则,但不含有修订语言标准中所引入的语言新特性(例如标记类型、子单元或十进制类型)用法的特定准则。
文档大致按照 Ada Reference Manual [ISO 8052] 的结构来组织。
第 2 章『简介』解释准则所基于的基本原则并介绍了准则分类。
第 3 章『代码布局』说明程序文本的一般视觉组织结构。
第 4 章『注释』指导读者如何以结构化、有用和可维护的方式使用注释来说明代码。
第 5 章『命名约定』提供有关命名语言实体的一般规则和示例。必须对本章进行定制以适合特定项目或组织的需要。
第 6 章『声明』和第 7 章『表达式和语句』对每种语言构造提供进一步的建议。
第 8 章『可视性问题』和第 9 章『程序结构和编译问题』提供有关程序全局构建和组织的指导。
第 10 章『并行』说明使用语言中与任务和时间相关的特性的专门主题。
第 11 章『错误处理和异常』就如何使用或不使用异常以便通过轻量级系统化的方式处理错误提供指导。
第 12 章『低级别编程』说明表示子句的问题。
第 13 章『总结』概括最重要的准则。
本文档取代了 Ada Guidelines: Recommendation for Designers and programmers, Application Note #15, Rational, Santa Clara, CA., 1990。
Ada 语言设计的明确目的就是支持开发高质量、可靠、可复用和可移植的软件 [ISO 87, sect. 1.3]。然而,没有哪一种编程语言仅靠其本身就可以确保实现上述目标。编程必须是一个有良好规范的过程的一部分。
清晰而易于理解的 Ada 源代码是此处提供的大多数准则的主要目标。这一点是实现可靠性和可维护性的主要作用因素。所谓清晰而易于理解的代码可以从以下三条简单的基本原则上来理解。
在源代码生命期内,代码更多地是被读,而不是被写,尤其是它的说明部分。理想情况下,代码读起来应该象一篇说明它做了什么的英文描述,它的另一个附加优点是可以执行。 程序主要是为人编写的,而不是为计算机编写的。阅读代码是一个复杂的脑力过程,它可通过统一性来支持,在本指南中也称为最小限度意外原则。软件开发人员使用编程标准主要是为了统一整个项目的风格,不应将其理解为是某种处罚或阻碍创造力和生产力的阻碍。
本指南所基于的另一个重要原则是单点维护原则。应尽量使设计决策只在 Ada 源码中的一处表达,它的多数结论应程序化地从该处派生出来。违背这一原则将大大地有损可维护性、可靠性以及可读性。
最后,作为易读性的主要作用因素,应用最小限度干扰原则。即,已尽量避免在源代码中混入视觉“干扰”:栏、框和其他低信息量的文本或对理解软件用途没有帮助的信息。
可移植性和可复用性也是制定许多准则的原由。代码应可移植到不同目标计算机的几个不同编译器上,并最终移植到一个更高级的 Ada 版本(“Ada 95”)上 [PLO92, TAY92]。
此处给出的准则具有以下几条基本假设:
应鼓励在任何可带来好处的地方使用 Ada 的高级特性,而不要仅仅因为一些程序员不熟悉它们而不鼓励使用。 这是项目能从使用 Ada 中真正获益的唯一方式。Ada 不应象 Pascal 或 FORTRAN 语言那样使用。不鼓励在注释中解释代码;相反,Ada 本身应尽可能地取代注释。
许多命名约定在词汇和语法上基于英文。此外,Ada 的关键字都是常用的英语单词,将它们与其他语言混合将降低代码的易读性。
命名约定和其他一些规则假定“use”子句不被运用。
许多规则在大型 Ada 系统中最有价值,尽管如果仅是出于项目级或公司级的实践做法和统一性的目的,它们也可用于小型系统。
通过运用 Rational 环境,诸如代码布局、封闭构造中的标识等的问题就由 Ada 编辑器和格式化程序处理了。 但是本文档中包含的布局建议可应用于任何开发平台。
许多规则支持将面向对象(OO)的概念系统地映射到 Ada 特性和特定命名约定。
这些准则并非同等重要。它们大致遵循以下的等级:
该准则仅是一条简单的建议;不遵守它也不会有什么实质性的损害,使用或是不使用它仅是个人喜好问题。本文档中的提示用以上符号标记。
该准则通常更多地基于技术的观点;它可能会影响到代码的可移植性或可复用性,并在一些实施中影响性能。 除非有合适的理由,否则必须遵守建议。本文档中提到了一些例外的情况。本文档中的建议用以上符号标记。
有疑问的特性使用起来危险,但它并不被完全禁止;应用该特性的决定应在项目级上做出,并且决定应该是高度可见的。本文档中的限制用以上符号标记。
违背它定将导致错误、不可靠或不可移植的代码。 要求不可违背。本文档中的要求用以上手指图标标记。
使用 Rational 设计工具来标记受限特性的用法并强制实施必需规则和许多建议。
与许多其他的 Ada 编码标准不同的是:事实上这些准则中极少有完全禁用的 Ada 特性。好软件的关键在于:
使用常识。
当您无法找到规则或准则,当规则明显不适用或当其他一切方法都失败时:请使用常识并核查基本原则。此规则优先于其他所有规则。常识是必不可少的。
一个程序单元的布局完全在 Rational 环境格式化程序的控制之下,除了在注释和空白处,程序员不必过多担心程序的布局。这个工具所使用的格式约定在 Reference Manual for the Ada Programming Language [ISO87] 的 Appendix E 中表述。 他们特别建议结构化构造的开始和结束处的关键字应垂直对齐。一个构造的标识也应系统性地在构造的末尾重复。
格式化程序的确切行为由一系列库开关控制,根据一个通用模型世界,这些库开关在整个项目过程中接收一套统一的值。以下列出了相关的开关以及它们对于所建议的模型世界的当前值。
指定 Ada 单元中标识的大小写:最开始的字母以及每个下划线之后的第一个字母用大写。大写形式加上最现代的屏幕和激光打印机字体是读者最易读的形式。
指定 Ada 关键字的大小写。这使它们与标识略微有所区分。
指定浮点字面值中字母“E”和基本字面值中基本数字(“A” 到 “F”)的大小写。
一个 Ada 单元按照 Ada Reference Manual [ISO87] 的 Appendix E 所述的一般约定来编排格式。这就是说一个结构化构造的开始和结束处的关键字应该对齐。 例如,“loop”和“end loop”,“record”和“end record”。在结构化构造内的元素应向右缩进。
指定格式化程序对“if”语句、“case”语句和“loop”语句这样的结构化(主要)构造缩进的列数。
说明格式化程序使下列次要构造缩进的列数: 记录的声明、变体记录的声明、类型的声明、异常处理程序、备选项、case 语句以及命名和有标号语句。
指定换行前格式化程序显示 Ada 单元中的行所使用的列数。这使得格式化的单元可用传统的 VT100 之类的终端显示。
指定当一条语句长于 Line_Length 而必须要断开时,格式化程序应使第二行和后续行缩进多少列。只有当不存在词法构造可让缩进的代码对齐时,格式化程序才令代码缩进 Statement_Indentation 个列。
指定当显示一个语句时,每行所保留的列数。 如果当前级别的缩进所允许的一行中的列数小于 Statement_Length,那么格式化程序将从 Wrap_Indentation 列的位置开始,这将作为新级别的缩进。此做法可防止嵌套太深的语句显示时超出右边界。
指定当前级别的缩进不足 Statement_Length 时,格式化程序在哪一列处开始下一级别的缩进。此做法可防止嵌套太深的语句显示时超出右边界。
控制形如(xxx:aaa; yyy:bbb)的列表格式,该列表出现在子程序的形参部分,或者作为类型声明中的判别式。它也控制形如(xxx=>aaa, yyy=>bbb)的列表格式,该列表出现在子程序调用和聚集处。因为该选项非零(真),所以当一个列表无法在一行中显示时,列表中的每一个元素都占用新的一行。
指定为使连续语句中的词法构造对齐(如命名符号中的冒号、赋值号和箭头),格式化程序可插入的空格数。如果对齐一个构造所需的空格数多于这个数,那么构造不会被对齐。
要注意的是,为了形成某种布局,程序员可以插入一个行结束符或者通过输入 <space> <space> <carriage-return> 来敲入一个不会被格式化程序删掉的换行符。
当列表中的元素超过 3 个并且在一行中显示不了这些元素时,使用该方法:Ada 元素列表应分隔为一行仅有一个元素,以提高代码的易读性和可维护性。这一条尤其适用于以下 Ada 构造(如 Ada Reference Manual [ISO87] 的 Appendix E 中所定义):
自变量关联
pragma Suppress (Range_Check, On => This_Type, On => That_Type, On => That_Other_Type);
标识列表,组件列表
Next_Position, Previous_Position, Current_Position : Position; type Some_Record is record A_Component, B_Component, C_Component : Component_Type; end record;
枚举类型定义
type Navaid is (Vor, Vor_Dme, Dme, Tacan, VorTac, NDB);
判别式约束
subtype Constrained is Element (Name_Length => Name'Length, Valid => True, Operation => Skip);
语句的顺序(由格式化程序处理)
形参部分、类属形参部分、实参部分、类属实参部分
procedure Just_Do_It (This : in Some_Type; For_That : in Some Other_Type; Status : out Status_Type); Just_Do_It (This => This_Value; For_That => That_Value; Status => The_Status);
与广泛接受的观点相反的是,好程序的特点并非是注释的数量,而是注释的质量。
注释应当作为 Ada 代码的补充,而不是解释。Ada 本身是一种非常易读的程序设计语言,尤其有好的命名约定支持时。注释应当通过解释那些不能直接从 Ada 代码中看出的内容来对代码作出补充;它们不应复制 Ada 的语法或语义。注释应当帮助读者掌握背景概念和依赖关系,特别是复杂的数据编码或算法。注释应当突出与编码或设计标准的不同点、受限特性的使用以及特殊的“技巧”。在每个主要的 Ada 构造(如子程序和程序包)中,系统地出现的注释框架或表单具有一致性和提醒程序员说明代码的优点,但它们也常常带来释义代码的风格。对每个注释,程序员都应能够很好地回答:“这个注释有何价值?”
一个误导性的或错误的注释比完全没有注释还糟。编译器不会检查注释(除非它们象 Rational 设计工具那样参与了正式的 Ada 设计语言(ADL)或者程序设计语言(PDL))。因此根据单点维护原则,设计决定应在 Ada 而不是注释中表述,即使这样做的代价是需要多作几个声明。
作为一个(并非很好的)示例,请考虑如下的声明:
------------------------------------------------------------ -- procedure Create ------------------------------------------------------------ -- procedure Create (The_Subscriber: in out Subscriber.Handle; With_Name : in out Subscriber.Name); -- -- Purpose: This procedure creates a subscriber with a given -- name. -- -- Parameters: - The_Subscriber :mode in out, type Subscriber.Handle - It is the handle to the created subscriber - With_Name :mode in, type Subscriber.Name - The name of the subscriber to be created. - The syntax of the name is -- <letter> { <letter> | <digit> } -- Exceptions: -- Subscriber.Collection_Overflow when there is no more -- space to create a new subscriber -- Subscriber.Invalid_Name when the name is blank or -- malformed -- -------------------------------------------- end Create ----
可以从这个例子中得出几个要点。
在这个例子中,最好使用以下更简洁、更有用的方式:
procedure Create (The_Subscriber : in out Subscriber.Handle; With_Name : in Subscriber.Name);-- --Raises Subscriber.Collection_Overflow. --Raises Subscriber.Invalid_Name when the name is --blank or malformed (see syntax description --attached to declaration of type Subscriber.Name).
注释应放在相关代码的旁边,缩进同样的大小,紧靠着代码 - 即用空的注释行在视觉上将注释块与 Ada 构造捆在一起:
procedure First_One; -- -- This comment relates to First_One. -- But this comment is for Second_One. -- procedure Second_One (Times : Natural);
用空行分隔相关的源码块(注释和代码)而不用繁复的注释行。
在单个注释块内使用空注释而不是空行来分隔段落:
-- Some explanation here that needs to be continued in a -- subsequent paragraph. -- -- The empty comment line above makes it clear that we -- are dealing with a single comment block.
虽然注释可以放在相关 Ada 构造的上方或下方,但是要将诸如节标题或适用于若干个 Ada 构造的主要信息之类的注释放在构造的上方。将评述或附加信息等这一类的注释放在所适用 Ada 构造的下方。
用整个页宽在 Ada 构造的开头处对注释分组。避免将注释放在 Ada 构造的同一行。这些注释常常变得不对齐。但是在冗长声明中描述每个元素(如枚举类型字面值)时,这样的注释方式是可以容许的。
用标准注释块的小型层次结构来说明节标题,但这仅仅适用于很大的 Ada 单元(>200 个的声明或语句):
--=========================================================== -- -- 主标题 -- --=========================================================== ------------------------------------------------------------- -- 副标题 ------------------------------------------------------------- -- -------------------- 子节标题 -- --------------------
在这种标题注释前面所放的空行数要多于后面的空行数,例如两行在前一行在后。这样使标题与随后的文本在视觉上相联系。
避免使用含有诸如作者、电话号码、创建和修改日期、单元位置(或文件名)之类信息的标题,因为这些信息很快就会过时。将所有者版权声明放在单元的末尾,尤其是使用 Rational 环境时。当访问一个程序包的规格说明源码时(例如在 Rational 环境中按 [Definition]),用户不想浏览两三页对理解程序无任何用处的文本和/或完全不带有任何程序信息的文本(如版权声明)。避免使用垂直滚动条、封闭图文框或方框,这些东西只会增加视觉干扰,而且难以保持一致性。用 Rational CMVC 附注(或者其他形式的软件开发文件)来保存单元历史。
不要重复通常可以在其他地方找到的信息;仅提供一个指向该信息的指针即可。
尽量使用 Ada,而非注释。要实现这一点,你可使用更好的名称、额外的临时变量、限定、重命名、子类型、静态表达式和属性,这些都不影响生成的代码(至少在一个好的编译器中不影响)。您也可以使用小的、嵌入式的谓词函数,并将代码分成几个无参过程,这些过程的名称提供了代码中几个离散部分的标题。
示例:
替换:
exit when Su.Locate (Ch, Str) /= 0; -- Exit search loop when found it.
Search_Loop : loop
Found_It := Su.Locate (Ch, Str) /= 0;
exit Search_Loop when Found_It
end Search_Loop;
替换:
if Value < 'A' or else Value > 'Z' then -- If not in uppercase letters.
subtype Uppercase_Letters is Character range 'A' .. 'Z'; if Value not in Uppercase_Letters then ...
替换:
X := Green; -- This is the Green from -- Status, not from Color. raise Fatal_Error; -- From package Outer_Scope. delay 384.0; -- Equal to 6 minutes and 24 -- seconds.
The_Status := Green;
X := Status'(Green); raise Outer_Scope.Fatal_Error; delay 6.0 * Minute + 24.0 * Second;
替换:
if Is_Valid (The_Table (Index).Descriptor(Rank).all) then -- This is the current value for the iteration; if it is -- valid we append to the list it contains. Append (Item, To_List => The_Table (Index).Descriptor(Rank).Ptr);|
declare Current_Rank : Lists.List renames The_Table (Index).Descriptor (Rank); begin if Is_Valid (Current_Rank.all) then Append (Item, To_List => Current_Rank.Ptr); end if; end;
注意注释中的风格、语法和拼写。不要使用象电报密码一般的风格。使用拼写检查程序。(在 Rational 环境中调用 Speller.Check_Image)。
不要使用重音字母或其他非英文字符。根据 Ada Issue AI-339,某些开发系统可能支持非英文字符,某些 Ada 编译器仅在注释中支持。但这是不可移植的,并且有可能在其他系统上失效。
对于子程序,至少说明:
对于类型和对象,要说明所有不能通过 Ada 表达的不变量或附加约束。
避免注释中的重复。例如,用途的部分应是对“它做什么?”这个问题的简要回答,而不是“它怎么做?”的回答。概述应是设计的简要表达。说明中不应描述所用的算法,而应解释程序包应如何使用。
数据结构和算法部分应含有足够的信息来帮助理解主要的实施策略(以便正确使用程序包),但不需要包含所有的实施细节或与正确使用该程序包不相关的信息。
给 Ada 实体(程序单元、类型、子类型、对象、字面值、异常)指定好名称是所有软件应用程序中要应对的最精巧的事情之一。中等规模到大规模的应用程序存在着另一个问题:名称间的冲突,或是难以找到足够的同义词来指定关于同一现实世界概念的不同但又类似的表示方法(或是命名一个类型、子类型、对象、参数)。这里可利用不使用“use”子句(或只在极为受限的条件下使用)的规则。在许多情形下,这将允许缩短名称并复用相同的描述词,从而避免混淆的风险。
选择清晰、易读、有意义的名称。
与其他许多编程语言不同,Ada 不将标识的长度限制在 6、8 或 15 个字符内。短名称输入起来快并不是可以为人接受的理由。只有一个字母的标识通常表示选择不当或是懒惰。可能有几个例外,如将 E 作为自然对数的底、Pi 以及其他少数几个易辨识的情况。
用下划线将一个名称中的不同单词分隔开来:
Is_Name_Valid
,而不是 IsNameValid
使用全名而不是缩写。
只使用项目认可的缩写。
如果要使用缩写,则只能是在两种情况之一时:该缩写在应用领域中很普遍(如 FFT 代表快速傅立叶变换)或者来自公认缩写的项目级列表。否则,很有可能使相似但不相同的缩写到处出现,导致以后的混淆和错误(如将 Track_Identification 缩写成 Tr_Id、Trck_Id、Tr_Iden、Trid、Tid 或 Tr_Ident 等)。
少用后缀说明 Ada 构造的分类。它们不能提高易读性。
按照 Ada 实体的类别来加后缀(如给程序包加上 _Package,异常加上 _Error,类型加上 _Type,以及子程序的参数加上 _Param),这通常对帮助阅读和理解代码不是很有效。 而加上如 _Array、_Record 和 _Function 这样的后缀则更糟。Ada 编译器和读者都可以根据上下文从子程序中分辨一个异常:显然只有一个异常名可以在 raise 语句或异常处理程序中出现。这些后缀在如下受限的条件下是有用的:
类属形参类型后缀为 _Constrained
后缀为 _Pointer 的访问类型或其他非直接引用的形式:_Handle 或 _Reference
通过 _Or_Wait 隐藏了可能阻塞的入口调用的子程序
将名称表述出来,从而使它们以使用角度看的话还不错。
试想一个导出实体可在其中使用的上下文,并从那个角度选择一个名称。一个实体仅被声明一次,但却被使用许多次。这一点对于子程序的名称及其参数来说尤为正确; 使用命名关联产生的调用应尽量与自然语言相近。请记住缺少 use 子句使得大多数声明实体必须具有限定名。对于有可能在类属单元中比在客户端那里使用得还多的类属形参来说,必须寻找一个好的协调办法,但是绝对要优先考虑使子程序的形参在客户端外观好看。
使用英文单词并拼写正确。
语言的混合(如法语和英语)将使代码读起来比较困难,有时会对标识的含义引入歧义。 既然 Ada 的关键字已经是英文的,那么必须使用英文单词。最好使用美式拼法,以便利用 Rational 环境中内置的拼写检查程序。
不要重新定义 Standard 程序包中的任何实体。这是绝对禁止的。
这样做将产生混淆和严重的错误。该规则可延伸至其他预定义的库单元:Calendar、System。这也包括 Standard 标识本身。
避免对其他预定义程序包中的标识重定义,如 System 或 Calendar。
不要将如下内容用作标识:Wide_Character 和 Wide_String,它们将在 Ada 95 的 Standard 程序包中引入。不要引入一个名为 Ada 的编译单元。
不要使用以下单词作为标识:abstract、aliased、protected、requeue、tagged 和 until,它们在 Ada 95 将成为关键字。
下面是各 Ada 实体的一些命名建议。假定采用一般而言属于“对象风格”的设计样式。进一步的说明请参阅附录 A。
当一个程序包引入了某个对象类时,用该对象类的名称命名程序包,通常是一个单数形式的常见名词,如果需要(即如果定义了一个参数化的类)可加上后缀 _Generic。仅当对象总以组的形式出现时,才使用复数形式。例如:
package Text is package Line is package Mailbox is package Message is package Attributes is package Subscriber is package List_Generic is
如果一个程序包指定了一个接口或者功能分组,并且它不与任何对象相关,则应在它的名称中表达出来:
package Low_Layer_Interface is package Math_Definitions is
当一个“逻辑”程序包需要用平面分解来表达为几个程序包时,请使用在项目级达成一致的列表中的后缀。例如一个逻辑程序包 Mailbox 可如下实施:
package Mailbox_Definitions is package Mailbox_Exceptions is package Mailbox_Io is package Mailbox_Utilities is package Mailbox_Implementation is package Mailbox_Main is
其他可接受的后缀有:
_Test_Support _Test_Main _Log _Hidden_Definitions _Maintenance _Debug
在定义对象类的程序包中,请使用:
type Object is ...
前提是其中隐含了复制语义,即类型可实例化并且某种形式的赋值可行。请注意类名不应在标识中重复,因为它总是以标准形式使用:
Mailbox.Object Line.Object
当暗含共享语义时(即类型用存取值(或者其他非直接的形式)实施,并且赋值(如果存在)不复制对象),请使用如下方式说明这一点:
type Handle is
用于非直接引用type Reference is
是一个可能的备选方案
这些元素单独使用(前面加上程序包名)不清晰或有歧义,它们用作后缀。
当暗含多个对象时,请使用以下代码之一:
type Set
type List
type Collection
type Iterator
对于对象的字符串指定,请使用:
type Name
为了达到更好的易读性,还应该在整个定义程序包内使用类型的标准名。在 Rational 环境中,当在子程序调用中使用 [Complete] 函数时,上述方法也可导致更好的行为。
例如,请注意下面的全名 Subscriber.Object:
package Subscriber is type Object is private; type Handle is access Subscriber.Object; subtype Name is String; package List is new List_Generic (Subscriber.Handle); Master_List : Subscriber.List.Handle; procedure Create (The_Handle : out Subscriber.Handle; With_Name : in Subscriber.Name); procedure Append (The_Subscriber : in Subscriber.Handle; To_List : in out Subscriber.List.Handle); function Name_Of (The_Subscriber : Subscriber.Handle) return Subscriber.Name; ... private type Object is record The_Name : Subscriber.Name (1..20); ... end Subscriber;
在其他情况下,类型名称使用名词或者限定词+名词。类型可用复数形式,而让对象(变量)名使用单数:
type Point is record ... type Hidden_Attributes is ( ... type Boxes is array ...
对于枚举类型,请将 Mode、Kind 和 Code 等单词单独使用或者作为后缀使用。
对于数组类型,当已将简单名用于组件类型时,可以使用后缀 _Table。仅当数组用暗含语义来维护时,才使用象 _Set 和 _List 这样的名称或后缀。请保留 _Vector 和 _Matrix 作为相应的数学概念。
因为要避免单数的任务对象(原因在后文解释),所以即使只有一个该类型的对象时,也应引入任务类型。下面是诸如 _Type 之类的令人满意的简单后缀策略的例子:
task type Listener_Type is ... for Listener_Type'Storage_Size use ... Listener : Listener_Type;
同样,当类型名使用名词(或者名词短语)存在冲突时,或者几处的对象名或参数名有冲突时,在该类型名词后加上后缀 _Kind,而让对象使用简单(无后缀)名词:
type Status_Kind is (None, Normal, Urgent, Red); Status : Status_Kind := None;
或者,对于总以复数出现的事物的类型使用复数形式。
因为访问类型具有内在的危险性,所以使用者应小心。通常,它们称为 Pointer。如果名称本身有歧义,则应加上后缀 _pointer。作为备选方案,也可使用 _Access 做后缀。;
有时使用嵌套子程序包来引入辅助抽象可以简化命名:
package Subscriber is ... package Status is type Kind is (Ok, Deleted, Incomplete, Suspended, Privileged); function Set (The_Status : Subscriber.Status.Kind; To_Subscriber : Subscriber.Handle); end Status; ...
因为异常只能用于处理错误情况,所以应使用名词或名词短语来清楚地表达一种负面的含义:
Overflow, Threshold_Exceeded, Bad_Initial_Value
当在一个类程序包中定义异常时,标识不需要包含类名(例如 Bad_Initial_Subscriber_Value,因为该异常将始终用作 Subscriber.Bad_Initial_Value)。
使用 Bad、Incomplete、Invalid、Wrong、Missing 或 Illegal 中的一个单词作为名称的一部分,而不是笼统地使用不传达任何具体信息的 Error:
Illegal_Data, Incomplete_Data
过程(和任务入口)使用动词。对于函数使用带有对象类属性或特性的名词。对返回布尔值(谓词)的函数使用形容词(或过去分词)。s
Subscriber.Create Subscriber.Destroy Subscriber.List.Append Subscriber.First_Name -- Returns a string. Subscriber.Creation_Date -- Returns a date. Subscriber.List.Next Subscriber.Deleted -- Returns a Boolean. Subscriber.Unavailable -- Returns a Boolean. Subscriber.Remote
对于谓词,在某些情况下给名词前面加上前缀 Is_ 或 Has_ 或许有帮助;时态要准确和一致:
function Has_First_Name ... function Is_Administrator ... function Is_First... function Was_Deleted ...
当简单名称已作为类型名或枚举字面值时,上述方法有用。
谓词应使用肯定式,也就是说其中不应含有“Not_”。
对于普通操作,统一从项目可选列表(列表随着我们对系统认识的加深而扩展)中选择动词:
Create Delete Destroy Initialize Append Revert Commit Show, Display
谓词函数和布尔型的参数使用肯定式的名称。否定式的名称可能产生双重否定(例如 Not Is_Not_Found),从而使代码难读。
function Is_Not_Valid (...) return Boolean procedure Find_Client (With_The_Name : in Name; Not_Found : out Boolean)
应定义成:
function Is_Valid (...) return Boolean; procedure Find_Client (With_The_Name: in Name; Found: out Boolean)
这让客户端按需要实现了表达式的否定(这样做不会导致运行时间延长):
if not Is_Valid (...) then ....
有些情况下,通过使用反义词,如用“Is_Invalid”来代替“Is_Not_Valid”,也可使否定谓词在不改变语义的情况下变成肯定式。但是,肯定式的名称更易读:“Is_Valid”比“not Is_Invalid”更易理解。
当蕴涵着大致相同的意义时,使用相同的单词而不是寻找同义词或变体。因此鼓励按照最小限度意外原则使用重载来提高代码的一致性。
如果子程序被用做入口调用的“外表程序”或者“包装器”,则通过给动词加上后缀 _Or_Wait 或通过 Wait_For_ 之类的短语后跟名词,以使名称反映出这一点,可能很有用:
Subscriber.Get_Reply_Or_Wait Subscriber.Wait_For_Reply
一些操作应始终使用相同的名称来统一定义:
对于转换到字符串和从字符串转换来的类型变换,对称函数是:
function Image and function Value
对于转换到低级表示法和从低级表示法转换过来的类型变换(如用于数据交换的 Byte_String),对称过程是:
procedure Read and Write
对于分配数据:
function Allocate (rather than Create) function Destroy (or Release, to express that the object will disappear)
当使用统一的命名系统地处理它们时,类型组合将容易许多。
对于活动的迭代器,必须始终定义以下原语:
Initialize Next Is_Done Value_Of Reset. 如果要在相同的作用域范围内引入几个迭代器类型,那么应重载这些原语,而不是为每个迭代器分别引入一套各不相同的标识。参考资料 [BOO87]。
当使用 Ada 预定义属性作为函数名时,要保证其用法与一般的语义相同:'First、'Last、'Length、'Image 和 'Value 等。
请注意,几个属性(如 'Range 和 'Delta)是不能用做函数名的,因为它们是保留字。
为表明其唯一性或是表明该实体是操作的主要针对对象,请在对象或参数名称前加前缀 The_ 或 This_。要说明一个次要、临时、辅助的对象,则加前缀 A_ 或 Current_:
procedure Change_Name (The_Subscriber : in Subscriber.Handle; The_Name : in Subscriber.Name ); declare A_Subscriber : Subscriber.Handle := Subscriber.First; begin ... A_Subscriber := Subscriber.Next (The_Subscriber); end;
对于布尔型的对象,使用肯定式的谓词子句:
Found_It Is_Available
必须避免 Is_Not_Available
。
对于任务对象,请使用暗示活动实体的名词或名词短语:
Listener Resource_Manager Terminal_Driver
对于参数,在类名或者一些典型名词前加上一个介词前缀也可以增加代码的易读性,尤其是在使用了命名关联的调用方那里。辅助参数的其他有用前缀的形式包括 Using_,或者如果有一个 in out 参数作为辅助效果受到影响时,则前缀形式是 Modifying_:
procedure Update (The_List : in out Subscriber.List.Handle; With_Id : in Subscriber.Identification; On_Structure : in out Structure; For_Value : in Value); procedure Change (The_Object : in out Object; Using_Object : in Object);
从调用者的角度讲,参数定义的顺序也是非常重要的:
这样便可利用缺省值,而主参数不必使用命名关联。
即使在函数中,都应明确指出“in”模式。
对非类属版本选择一个您想用的最好的名称:对程序包使用类名,对过程(参见上文)使用及物动词(或者动词短语),并在名称后加上后缀 _Generic。
对于类属形参类型,当类属程序包定义了某个抽象数据结构时,使用
Item
或者 Element
作为类属形参,使用 Structure
或者其他更为合适的名词作为导出抽象。
对于被动的迭代器,在标识中使用如
Apply
、Scan
、Traverse
、Process
或 Iterate
这样的动词:
generic with procedure Act (Upon : in out Element); procedure Iterate_Generic (Upon : in out Structure);
类属形参名不能是同形异义字。
generic type Foo is private; type Bar is private; with function Image (X : Foo) return String; with function Image (X : Bar) return String; package Some_Generic is ...
应被替代为:
generic type Foo is private; type Bar is private; with function Foo_Image (X : Foo) return String; with function Bar_Image (X : Bar) return String; package Some_Generic is ...
如果需要,类属形参可以在类属单元中重命名:
function Image (Item : Foo) return String Renames Foo_Image; function Image (Item : Bar) return String Renames Bar_Image;
当一个大型系统被分割成几个 Rational 子系统(或者其他形式的互连程序库)时,定义一个满足以下各项的命名策略将很有用:
在一个含有几百个对象和子对象的系统里,在库单元级很有可能产生一些名称冲突。对于一些很有用的象 Utilities、Support、Definitions 等这样的名称,程序员缺乏同义词。
在 Rational 主机上使用浏览工具找到实体在何处定义是很简单的,但是当代码被移植到一个目标计算机并且使用目标机上的工具(调试器、测试工具等等)时,要在 100 个子系统的 2000 个单元中找到过程 Utilities.Get 的位置,这对于一个项目新手来说可能就不那么容易了。
在库级单元名前加上四个字母的缩写作为前缀表示它所属子系统。
子系统的列表可在软件体系结构文档(SAD)中找到。这条规则对高度可复用组件(即可能跨众多项目、COTS 产品和标准单元复用)的库除外。
示例:
Comm Communication
Dbms Database management
Disp Displays
Math Mathematical packages
Drive Drivers
例如,从子系统 Disp 中导出的所有库单元要加上前缀 Disp_,以使负责 Disp 的小组或团队在命名上有完全的自由。如果 DBMS 和 Disp 都要引入一个名为 Subscriber 的对象类,将会产生如下程序包:
Disp_Subscriber Disp_Subscriber_Utilities Disp_Subscriber_Defs Dbms_Subscriber Dbms_Subscriber_Interface Dbms_Subscriber_Defs
Ada 的强类型化工具可用来防止混用不同类型。概念不同的类型必须作为不同的用户定义类型来实现。应该使用子类型来提高程序的可读性以及增强编译器生成的运行时检查的有效性。
尽可能在枚举中引入一些额外的字面值来表示未初始化、无效或是完全没有值:
type Mode is (Undefined, Circular, Sector, Impulse); type Error is (None, Overflow, Invalid_Input_Value,Ill-formed_Name);
这将支持系统地初始化对象的规则。将这个字面值放在列表的开头而不是末尾,这样便于维护而且允许如下所示的有效值连续子域相邻:
subtype Actual_Error is Error range Overflow .. Error'Last;
避免使用预定义的数值类型。
当目标是高度的可移植性和可复用性,或是需要控制数值对象所占用的内存空间时,不得使用预定义的数值类型(来自 Standard 程序包)。这样要求的原因是,预定义类型 Integer 和 Float 的特点在 Reference Manual for the Ada Programming Language [ISO87] 中(有意地)未加以说明。
首选的系统化策略是(举例来说,在程序包 System_Types 中)引入特定于项目的数值类型,其名称指示了精度或内存大小:
package System_Types is type Byte is range -128 .. 127; type Integer16 is range -32568 .. 32567; type Integer32 is range ... type Float6 is digits 6; type Float13 is digits 13; ... end System_Types;
不要重定义 Standard 程序包中的标准类型。
不要指定应从何种基本类型派生出来;让编译器来选择。
下面是一个较差的例子:
type Byte is new Integer range -128 .. 127;
Float6 是一个比 Float32 更好的名称,即使在大多数机器上 32 位的浮点数也可实现 6 位数精度。
在项目的不同部分,派生出一些类型,其名称比 Baty_System_Types 中的名称更有意义。某些精度最高的类型可作为专用类型,以支持具有有限精度支持的目标的最终端口。
这一策略在如下情形下使用:
如果不是这样,那么另一个更简单的策略就是始终定义新类型,指定要求的范围和精度,但不指定它们应该从哪个基本类型派生出来。例如,声明:
type Counter is range 0 .. 100; type Length is digits 5;
这要优于:
type Counter is new Integer range 1..100; -- could be 64 bits type Length is new Float digits 5; -- could be digits 13
第二种策略迫使程序员考虑每种类型所要求的准确上下限和精度,而不是任意地选择几位。但要注意如果范围与基本类型的范围不相同,编译器将系统地进行范围检查 - 例如在上述类型 Counter 中,如果基本类型是一个 32 位的整型的话即会如此。
如果范围检测成为一个问题,那么避免它们的一种方法是声明:
type Counter_Min_Range is range 0 .. 10_000; type Counter is range Counter_Min_Range'Base'First .. Counter_Min_Range'Base'Last;
避免标准类型通过象循环、指针范围等构造泄漏到代码中去。
预定义数值类型的子类型只在以下环境中使用:
示例:
for I in 1 .. 100 loop ... -- I is of type Standard.Integer type A is array (0 .. 15) of Boolean; -- index is Standard.Integer.
使用形式:Some_Integer range L .. H
for I in Counter range 1 .. 100 loop ... type A is array (Byte range 0 .. 15) of Boolean;
不要试图实施无符号类型。
Ada 中不存在无符号算术整数类型。在 Ada 语言定义里,所有整数类型都直接或间接地从预定义类型中派生出来,因此它们一定是零对称的。
为实现可移植性,只依靠其值在以下范围内的那些实数类型:
[-F'Large .. -F'Small] [0.0] [F'Small .. F'Large]
注意 F'Last 和 F'First 不能是模型数,甚至不能在任何模型区间内。F'Last 和 F'Large 的相对位置有赖于类型定义和基础硬件。一个特别糟的示例是一个定点类型的 'Last 不属于该类型的例子,如下所示:
type FF is delta 1.0 range -8.0 .. 8.0;
其中,根据 Ada Reference Manual 3.5.9(6) 中的一条严格的说法是,FF'Last = 8.0 不可属于该类型。
要表示很大或者很小的实数,使用属性 'Large 或者 'Small(以及它们相应的负数形式),而不是对整数类型使用的 'First 和 'Last。
对于浮点类型,仅使用 <= 和 >=,不使用 =、<、> 和 /=。
绝对值比较的语义定义得较差(在表达式上看是相等的,但在要求的精度范围内看是不等的)。例如,X < Y 产生的结果可能与 not (X >= Y) 产生的结果不同。A = B 相等性检测应表述成:
abs (A - B) <= abs(A)*F'Epsilon
要提高可读性和可维护性,可考虑提供一个 Equal 运算符来封装上面的表达式。
另外也请注意以下更简单的表达式:
abs (A - B) <= F'Small
仅在 A 和 B 的值较小时才有效,因此这种表达法一般不建议使用。
避免引用预定义异常 Numeric_Error。Ada 设计组的一个关联解释已使所有生成 Numeric_Error 的情形现在都生成 Constraint_Error。Ada 95 中已废弃 Numeric_Error 异常。
如果 Numeric_Error 仍在实施过程中产生(这是 Rational 本机编译器出现的情况),那么请始终在异常处理程序中随 Numeric_Error 一起检查 Constraint_Error(作为同一替代项):
when Numeric_Error | Constraint_Error => ...
注意下溢。
Ada 中不检测下溢。结果是 0.0,没有异常生成。请注意下溢的检测可如下明确实现:乘或者除 0.0,再检查其结果,这些操作数都不应是 0.0。另外请注意可以实施您自己的运算符来自动执行这种检测,虽然这会以效率为代价。
限制定点类型的使用。
尽量使用浮点类型。在一个 Ada 程序的实施中,定点类型的不均衡实施将带来可移植性问题。
对于定点类型,'Small 应该与 'Delta 相等。
代码中应指定这一点。'Small 的缺省选项是 2 的乘方将导致各种问题。一个让选项清晰明白的方法是按如下方式写代码:
Fx_Delta : constant := 0.01; type FX is delta Fx_Delta range L .. H; for FX'Small use Fx_Delta;
如果不支持定点类型的长度子句,那么唯一遵循这条规则的方法是明确说明 'Delta 是 2 的乘方。子类型可以具有与 'Delta 不同的 'Small(这一条规则仅适用于类型定义或者 Ada Reference Manual 中的术语“初次命名的子类型”)。
尽量给记录类型的组件提供简单、静态的初始值(通常可用 'First 或 'Last 这样的值)。
但这不适用于判别式。语言规则规定判别式总是有值的。仅当可变性是要求的特点时,才应引入可变记录(即判别式具有缺省值的记录)。否则,可变记录将引入额外的内存空间(通常会分配最大的变量)和时间(变量检测的实现更为复杂)开销。
避免在任何组件的初始缺省值下进行函数调用,因为这可能导致“精化前访问”错误(请参阅“程序结构和编译问题”)。
对于可变记录(判别式具有缺省值的记录),如果一个判别式在其他某个组件的维内使用,应将其指定到一个合理的小范围内。
示例:
type Record_Type (D : Integer := 0) is record S : String (1 .. D); end record; A_Record : Record_Type;
在大多数实施中会产生 Storage_Error。要给判别式 D 的子类型指定一个更合理的范围。
不要对记录的物理布局做任何假设。
特别是(与其他编程语言不同),组件不需要按照定义的顺序排列。
限制访问类型的使用。
这一点对于要在无虚拟内存的小型机器上长期运行的应用程序来说尤其正确。访问类型是危险的,因为小小的编程错误可能会导致存储耗尽,甚至即使有了好的编程也会产生内存碎片。访问类型还较慢。访问类型的使用应作为项目范围策略的一部分,应追踪集合、集合的大小、分配点和释放点。为了让抽象客户端意识到存取值受控制,选用的名称应指出这一点:使用 Pointer 命名或者在名称后面加上后缀 _Pointer。
在程序精化时分配集合,并系统地指定每个集合的大小。
(在存储单元中)给出的值可以是静态的,也可以是动态计算出来的(例如从一个文件中读取)。这条规则的理由是,宁愿让程序在启动时立刻失败,也不要让它神秘地在 N 天之后死掉。类属程序包可以给这个值提供附加的类属形参来指定其大小。
请注意每个分配的对象常常有一些开销:可能是出于内部管理目的,目标系统上的运行时给每个内存块分配了一些附加的信息。所以要存储 N 个对象,其大小为 M 个存储单元,可能需要给集合分配多于 N * M 个存储单元 - 例如 N * (M + K) 个。从 [ISO87] 的 Appendix F 或者从试验运行中获得此开销 K 的值。
封装分配符(Ada 原语 new)和 release 的使用。如果可行,应管理一个内部可用空间列表,而不是依靠 Unchecked_Deallocation。
如果用一个访问类型实施某个递归数据结构,则很有可能访问的记录类型(作为一个组件)具有该同一访问类型。不增加任何空间上的开销(除了指向列表头的指针外),通过将可用单元串联到一个可用列表中,可以回收这些单元。
明确地处理好由 new 生成的 Storage_Error 异常,并重新导出一个更有意义的异常来说明集合的最大存储大小已耗尽。
只在一处完成分配和释放还使得在遇到问题时进行跟踪与调试更加简单。
仅对同一大小(因此判别式也一样)的已分配单元执行释放操作。
这一点对于避免内存碎片很重要。 Unchecked_Deallocation 不大可能提供内存压缩服务。 你可能想检查一下运行时系统是否提供相邻释放块的结合功能。
系统化地给 Destroy(或者 Free、Release)原语提供访问类型。
这一点对于使用访问类型实施的抽象数据类型尤为重要,应系统化地进行以实现多个此种类型的组合。
系统地释放对象。
尝试映射对分配与释放的调用,以保证所有分配的数据都已释放。尝试将数据释放到分配数据的作用域内。请记住当异常发生时也要释放。请注意这只是使用 when others 选择语句并用一个 raise 语句结束的一种情形。
首选的策略是运用模式:Get-Use-Release。程序员得到(Get)对象(这将创建动态数据结构),然后使用(Use)它,最后必须释放(Release)它。要确保三个操作在代码中可清楚分辨,并且在框架的所有可能出口(包括异常)完成了释放。
释放临时的组合数据结构时要小心,它们可能含在记录中。
示例:
type Object is record Field1: Some_Numeric; Field2: Some_String; Field3: Some_Unbounded_List; end record;
其中,Some_Unbounded_List 是一个组合链接结构,即它由许多链接在一起的对象组成。现在来考虑一个典型的属性函数,形如:
function Some_Attribute_Of(The_Object: Object_Handle) return Boolean is Temp_Object: The_Object; begin Temp_Object := Read(The_Object); return Temp_Object.Field1 < Some_Value; end Some_Attribute_Of;
当对象被读入 Temp_Object 时,在堆中隐式创建的组合结构从未被释放,但现在却无法访问了。这是内存泄漏。正确的处理方法是,对这种代价高昂的结构实施 Get-Use-Release 范式。换句话说,客户端应首先得到(Get)对象,然后按需要使用(Use)它,再释放(Release)它:
procedure Get (The_Object : out Object; With_Handle : in Object_Handle); function Some_Attribute_Of(The_Object : Object) return Some_Value; function Other_Attribute_Of(The_Object : Object) return Some_Value; ... procedure Release(The_Object: in out Object);
客户端代码看上去可能是这样:
declare My_Object: Object; begin Get (My_Object, With_Handle => My_Handle); ... Do_Something (The_Value => Some_Attribute_Of(My_Object)); ... Release(My_Object); end;
每当需要隐藏实施细节时,请将类型声明为专用。
以下情况下,要用专用类型隐藏实施细节:
在 Rational 环境中,专用类型同封闭专用部分以及子系统一起,大大降低了预料外接口设计变化所带来的影响。
与所谓的“纯粹”面向对象程序设计恰好相反,当相应的完整类型是最好的可能抽象时,不要使用专用类型。讲求实效;问一问让类型专用是否可以增加什么价值。
例如,一个数学向量最好表示成一个数组,或平面上的一个点表示成一个记录,而不是将它们表示成一个专用类型:
type Vector is array (Positive range <>) of Float; Type Point is record X, Y : Float := Float'Large; end record;
数组下标、记录组件选择、聚集符号,都远比一系列的子程序调用要易读得多(并且最终更有效率),而这些调用是在类型毫无必要地专用后不可避免的。
当实际对象和值的缺省赋值或比较毫无意义、不直观或不可能时,将专用类型声明为受限的。
这是在如下条件下的情形:
受限专用类型应自初始化。
这种类型的对象声明必须接收一个合理的初始值,因为一般来说,要在以后赋初值并且不冒在子程序调用中产生异常的风险,是不切实际的。
只要可行或有意义,就给受限类型提供一个 Copy(或 Assign)过程和一个 Destroy 过程。
设计类属形参类型时,只要在内部不要求相等或赋值,则应指定受限专用类型,以提高相应类属单元的可用性。
与前面讲述的规则相一致,可以导入一个 Copy 和一个 Destroy 类属形参过程以及(如果有意义)一个 Are_Equal 谓词。
对于类属形参专用类型,要在说明中指出相应的实际量是否应受约束。
这可以通过命名约定和/或注释来实现:
generic --Must be constrained. type Constrained_Element is limited private; package ...
也可以使用 Rational 定义的编译指示 Must_Be_Constrained
:
generic type Element is limited private; pragma Must_Be_Constrained (Element); package ...
请记住派生一个类型也就派生了所有在父类型的同一声明部分声明的子程序:可派生子程序。因此不需要在派生类型的声明部分中将它们都作为外表程序重新定义。但是类属子程序不可派生,因此可能需要将其作为外表程序重新定义。
示例:
package Base is type Foo is record ... end record; procedure Put(Item: Foo); function Value(Of_The_Image: String) return Foo; end Base; with Base; package Client is type Bar is new Foo; -- At this point, the following declarations are -- implicitly made: -- -- function "="(L,R: Bar) return Boolean; -- -- procedure Put(Item: bar); -- function Value(Of_The_Image: String) return Bar; -- end Client;
因此也不需将这些操作重定义为外表程序。但要注意,类属子程序(例如被动的迭代器)不随着其他操作一起派生出来,因此必须作为外表程序重新导出。 在包含基本类型声明的说明之外的其他地方定义的子程序也不能派生,也必须作为外表程序重新导出。
应在对象声明中指定初始值,除非对象是自初始化的或暗含了一个初始缺省值(例如访问类型、任务类型、非判别式字段有缺省值的记录)。
所赋的值必须是一个真实、有意义的值,而不是该类型的一个任意值。如果实际的初始值可用(例如输入参数之一),则将值赋给它。如果不可能计算出一个有意义的值,则考虑以后再声明该对象,或者可能的话赋“nil”值。
名称“Nil”意味着“未初始化”,它被用来声明那种“不可用但已知值”的常量,这些常量可通过算法以受控方式拒绝。
尽量让 Nil 值只用于初始化,这样它的出现就能始终指示一个未初始化的变量错误。
注意有时也许不是对所有类型都能声明“Nil”值,尤其是在模运算类型中(例如角度)。这种情况下请选择较不可能出现的值。
请注意初始化大型记录的代码可能成本很大,尤其是当记录中存在变量并且一些初始值是非静态时(或者更准确地说是当值不能在编译时计算出来时)。有时一次性地完全精化一个初始值(可能在程序包中定义其类型)并明确地赋值会更有效:
R : Some_Record := Initial_Value_For_Some_Record;
注:
经验表明,在移植代码时未初始化的变量是问题产生的主要原因,也是编程错误的主要来源。 当开发主机想通过为至少部分对象提供缺省值来对程序设计者“更好”(例如,Rational 本机编译器上的 Integer 类型)时,或者当目标系统在程序装入之前将内存置零(例如在 DEC VAX 上)时,情况将更加恶化。要实现可移植性,永远要做最坏的假设。
当开销太大并且对象显然在使用前已经赋值时,在声明中可以省略赋初始值。
示例:
procedure Schmoldu is Temp : Some_Very_Complex_Record_Type; -- initialized later begin loop Temp := Some_Expression ... ...
在代码中避免使用字面值。
当定义的值绑定于一个类型时,请使用常量(带有一个类型)。 否则请使用已命名的数字,尤其是对所有无维值(纯值)来说:
Earth_Radius : constant Meter := 6366190.7; -- In meters. Pi : constant := 3.141592653; -- No units.
用全局的静态表达式来定义相关常量:
Bytes_Per_Page : constant := 512; Pages_Per_Buffer : constant := 10; Buffer_Size : constant := Bytes_Per_Page * Pages_Per_Buffer; Pi_Over_2 : constant := Pi / 2.0;
这利用了这些表达式必须在编译时准确计算出来这一点。
不要用匿名类型声明对象。有关更多信息,请参阅 Ada Reference Manual 3.3.1。
可维护性降低、对象无法作为参数传递并且经常会产生类型冲突错误。
子程序可被声明为过程或函数;以下是一些通用标准,可用来选择应以何种形式来声明。
在如下情况时声明一个函数:
在如下情形时将子程序声明为一个过程:
避免给用于缩放大小的结构(表、集合等)的类属形参赋缺省值。
编写局部过程时应尽可能减少副作用,函数应完全无副作用。说明副作用。
副作用通常是对全局变量的修改,并且可能只在读子程序主体时才注意到。在调用处程序员可能意识不到副作用。
作为参数传入必需的对象中可以增加代码的强壮性,使代码更易理解,更少依赖于它的内容。
这条规则主要适用于局部子程序;导出的子程序常常要求在程序包体内合法访问全局变量。
使用冗余的圆括号使复合表达式含义更加清晰。
表达式的嵌套层数定义为:在忽略运算符优先级规则的情况下,从左到右求表达式的值所需圆括号对的嵌套数。
将表达式的嵌套层数限制在四层以内。
记录聚集应使用命名关联并应该进行限定:
Subscriber.Descriptor'(Name => Subscriber.Null_Name, Mailbox => Mailbox.Nil, Status => Subscriber.Unknown, ...);
when others 在记录聚集中禁止使用。
这是因为与数组不同,记录本质上就是不同种类的结构,因此统一赋值是不合理的。
对于简单谓词,用简单的布尔表达式来取代“if...then...else”语句:
function Is_In_Range(The_Value: Value; The_Range: Range) return Boolean is begin if The_Value >= The_Range.Min and The_Value <= The_Range.Max then return True; end if; end Is_In_Range;
最好重写成:
function Is_In_Range(The_Value: Value; The_Range: Range) return Boolean is begin return The_Value >= The_Range.Min and The_Value <= The_Range.Max; end Is_In_Range;
如果影响可读性,那么含有两个或多个 if 语句的复杂表达式就不应这样修改。
循环语句在以下情形中应拥有名称:
Forever: loop ... end loop Forever;
如果循环有名称,那么它所含的任何 exit 语句应该指定该名称。
需要在一开始就进行完全测试的循环应使用“while”循环形式。需要在其他地方进行完全测试的循环应使用一般形式和一个 exit 语句。
最小化循环中的 exit 语句数。
在一个对数组进行迭代的“for”循环中,对数组对象使用 'Range 属性,而不是一个明确的范围或者某个其他子类型。
从循环中去掉所有独立于循环之外的代码。虽然“代码提升”是一种常用的编译器优化方法,但是当不变量代码调用其他编译单元时却不能使用。
示例:
World_Search: while not World.Is_At_End(World_Iterator) loop ... Country_Search: while not Nation.Is_At_End(Country_Iterator) loop declare City_Map: constant City.Map := City.Map_Of (The_City => Nation.City_Of(Country_Iterator), In_Atlas => World.Country_Of(World_Iterator).Atlas); begin ...
在上述代码中,对“World.Country_Of”的调用独立于循环之外(即在内循环中 country 保持不变)。 但是大多数情况下,是禁止编译器将调用移到循环之外的,因为调用有可能产生影响程序执行的副作用。每次在经过循环时,代码会因此而不必要地执行。
如果将代码重写成如下形式,循环将更有效率并更易理解和维护:
Country_Search: while not World.Is_At_End(World_Iterator) loop declare This_Country_Atlas: constant Nation.Atlas := World.Country_Of (World_Iterator).Atlas; begin ... City_Search: while not Nation.Is_At_End (The_City_Iterator) loop declare City.Map_Of ( The_City => Nation.City_Of (Country_Iterator), In_Atlas => This_Country_Atlas ); begin ...
子程序和入口调用应使用命名关联。
但是如果第一个(或者唯一的)参数明显是操作的主要对象(例如一个及物动词的直接对象)时,则只有这个参数的名称可省略:
Subscriber.Delete (The_Subscriber => Old_Subscriber);
其中 Subscriber.Delete 是及物动词,Old_Subscriber 是直接对象。下面无命名关联 The_Subscriber => Old_Subscriber 的表达式亦可接受:
Subscriber.Delete (Old_Subscriber); Subscriber.Delete (Old_Subscriber, Update_Database => True, Expunge_Name_Set => False); if Is_Administrator (Old_Subscriber) then ...
也可能有这样的情况:参数的意思很明显,命名关联反而会降低易读性。例如当所有参数都是相同类型和方式并且没有缺省值时便会如此:
if Is_Equal (X, Y) then ... Swap (U, B);
when
others 不应在 case 语句或者记录类型定义(对于变量)中使用。
每当离散类型定义被修改时,通过使这些构造失效来不使用 when others 在代码维护阶段很有用,这将迫使程序员考虑应如何处理那些修改。但是当选择器是一个大的整型范围时,它是可容许的。
当分支条件是一个离散值时,使用 case 语句而不是一系列“elsif”语句。
子程序应具有单个返回点。
尝试在语句部分的末尾从子程序里退出。函数应仅有一个返回语句。返回语句在函数体中自由放置则与 goto 语句的情况类似,会造成代码难以阅读和维护。
过程不应带有任何返回语句。
仅在很小的函数里才允许存在多个 return 语句,在这种情况下,所有 return 语句都能被同时看到,并且代码具有十分规则的结构:
function Get_Some_Attribute return Some_Type is begin if Some_Condition then return This_Value; else return That_Other_Value; end if; end Get_Some_Attribute;
限制 goto 语句的使用。
为“goto”语句说几句话:应注意到,goto 标注的语法和 Ada 中使用 goto 的受限条件使这个语句并不象所想的那样有害,而且在许多场合下,使用 goto 语句比使用与之等价的构造要好,要更易读,更有意义(例如一个带异常的伪 goto 语句)。
当对数组进行操作时,不要假定数组下标从 1 开始。应使用属性 'Last、'First 和 'Range。
定义无约束类型(大多是记录)的最常用约束子类型,并将这些子类型作为参数和返回值,以增加客户机代码的自检:
type Style is (Regular, Bold, Italic, Condensed); type Font (Variety: Style) is ... subtype Regular_Font is Font (Variety => Regular); subtype Bold_Font is Font (Variety => Bold); function Plain_Version (Of_The_Font: Font) return Regular_Font; procedure Oblique (The_Text : in out Text; Using_Font : in Italic_Font); ...
以下是建议的指南:
重载子程序。
但是当使用相同标识时,一定要确保所指的确是相同的操作。
避免嵌套作用域内隐藏同形异义标识。
这会使读者产生混淆,并且在维护时造成潜在的风险。也要小心“for”循环控制变量的存在和作用域。
不要在子类型上重载操作,一定要在类型上重载。
与一些天真的读者可能有的想法截然相反的是,重载将被运用到基本类型和它的所有子类型中去。
示例:
subtype Table_Page is Syst.Natural16 range 0..10; function "+"(Left, Right: Table_Page) return Table_Page;
当匹配子程序时,编译器会查找参数的基本类型,而非子类型。因此,上例中当前程序包内的所有 Natural16 值的“+”实际上都重定义了,而不仅仅是 Table_Page。因此,所有“Natural16 + Natural16”表达式现在都被映射到一个对“+”(Table_Page, Table_Page) 的调用,这样很可能返回错误的结果或者生成异常。
使由“with”子句引入的依赖关系个数最少。
在使用了“with”子句扩展可视性的地方,子句应尽量减小所覆盖的代码区。只在需要时才使用“with”子句,理想情况是只在主体或者一个大的体存根处使用。
使用接口程序包重新导出低级实体,这样可避免通过“with”子句以可见方式处理大量的低级程序包。要实现这一点,请使用派生类型、重命名、外表子程序以及可能的象字符串这样的预定义类型(正象 Environment 命令程序包一样)。
通过使用类属形参,使用单元间的软(弱)耦合,而不是通过使用“with”子句实现硬(强)耦合。
例如:要在一个组合类型上导出一个 Put 过程,则对其组件将某个 Put 过程作为类属形参量导入,而不是通过 with 子句直接使用 Text_Io。
不要使用“Use”子句。
尽量避免使用“use”子句可以提高代码可读性和易读性,前提是有效使用上下文的命名约定和适当重命名都充分支持这条规则。(请参阅上文中的“命名约定”)。这也有助于防止某些可视性意外,尤其是在代码维护阶段。
对于一个定义字符类型的程序包来说,在需要基于该字符类型定义字符串字面值的任何编译单元中,都必须使用“use”子句:
package Internationalization is type Latin_1_Char is (..., 'A', 'B', 'C', ..., U_Umlaut, ...); type Latin_1_String is array (Positive range <>) of Latin_1_Char; end Internationalization ; use Internationalization; Hello : constant Latin_1_String := "Baba"
不使用“use”子句避免了使用带中缀(infix)形式的运算符。那些运算符可以在客户机单元中重命名:
function "=" (X, Y : Subscriber.Id) return Boolean renames Subscriber."="; function "+" (X, Y :Base_Types.Angle) return Base_Types.Angle renames Base_Types."+";
因为无“use”子句常常使许多客户机单元中含有一套相同的重命名,所有这些重命名可在定义程序包本身中通过嵌入其中的 Operations 程序包分解。所以在客户机单元中推荐使用 Operations 程序包上的“use”子句:
package Pack is type Foo is range 1 .. 10; type Bar is private; ... package Operations is function "+" (X, Y : Pack.Foo) return Pack.Foo renames Pack."+"; function "=" (X, Y : Pack.Foo) return Boolean renames Pack."="; function "=" (X, Y : Pack.Bar) return Boolean renames Pack."="; ... end Operations; private ... end Pack; with Pack; package body Client is use Pack.Operations; -- Makes ONLY Operations directly visible. ... A, B : Pack.Foo; -- Still need prefix Pack. ... A := A + B ; -- Note that "+" is directly -- visible.
Operations 程序包应始终保持这个名称,并始终放在定义程序包可见部分的底端。“use”子句只在需要时出现,即如果在说明中未使用任何操作(通常是这样的),那么它只能放在客户机主体中。
with Defs; package Client is ... package Inner is use Defs; ... end Inner; -- The scope of the use clause ends here. ... end Client; declare use Special_Utilities; begin ... end; -- The scope of the use clause ends here.
使用重命名声明。
在限制使用“use”子句的同时推荐使用重命名,这可让代码易读。当一个具有很长名称的单元被引用几次时,给它取个短名称将提高易读性:
with Directory_Tools; with String_Utilities; with Text_Io; package Example is package Dt renames Directory_Tools; package Su renames String_Utilities; package Tio renames Text_Io; package Dtn renames Directory_Tools.Naming; package Dto renames Directory_Tools.Object; ...
短名称的选取应在整个项目中保持一致,以符合最小限度意外原则。实现方法是在程序包本身中提供短名称:
package With_A_Very_Long_Name is package Vln renames With_A_Very_Long_Name; ... end with With_A_Very_Long_Name; package Example is package Vln renames With_A_Very_Long_Name; -- From here on Vln is an abbreviation.
注意,程序包的重命名是将可视性仅赋予被重命名程序包的可见部分。
导入程序包的重命名必须在声明部分的开头分组,然后按照字母顺序排列。
重命名可在任何可提高易读性的地方局部使用(这样做不会有运行时损失)。类型可被重命名为子类型(无限制)。
正如有关注释部分所说的那样,重命名常常为说明代码提供了一种优雅并可维护的方式,例如给复杂对象取简单名或者局部细化类型的含义。选择重命名标识的作用域时应避免引入混淆。
重命名异常允许异常在几个单元中分解,例如在一个类属程序包的所有实例中分解。请注意在一个派生出类型的程序包中,可能由派生子程序产生的异常应与派生类型一起重新导出,以避免客户端不得不通过“with”来处理原始程序包:
with Inner_Defs; package Exporter is ... procedure May_Raise_Exception; -- Raises exception Inner_Defs.Bad_Schmoldu when ... ... Bad_Schmoldu : exception renames Inner_Defs.Bad_Schmoldu; ...
用“in”参数的不同缺省值重命名子程序可实现简单代码分解并增强易读性:
procedure Alert (Message : String; Beeps : Natural); procedure Bip (Message : String := ""; Beeps : Natural := 1) renames Alert; procedure Bip_Bip (Message : String := ""; Beeps : Natural := 2) renames Alert; procedure Message (Message : String; Beeps : Natural := 0) renames Alert; procedure Warning (Message : String; Beeps : Natural := 1) renames Alert;
避免在重命名声明的直接作用域内使用重命名实体的名称(旧名);只使用由重命名声明所引入的标识或运算符(新名称)。
多年以来,在 Ada 社区一直都有关于“use”子句的争议,有时甚至濒临一场信仰战争。双方都使用了各种论据或示例,但这些论据常常不能很好地扩展到大的项目上去,或是这些示例太不现实或明显不公允。
“use”子句的提倡者声称,子句提高了易读性;而且他们给出了非常不可读、冗长而重复的名称的例子,这些名称如果要使用几次,那么将其重命名会得到好处。他们还声称 Ada 编译器可以解析重载,的确如此,但是陷入一个大型 Ada 程序的人不可能象编译器那样可靠地完成重载解析,当然速度也没有这么快。他们声称象 Rational 环境这样复杂的 APSE 令显式的标准名称无甚用处;但这并不正确,因为使用者应不必对每一个不肯定的标识都按下 [Definition]。使用者应该不必猜测,但应能立即看出使用了哪些对象和抽象。 “use”子句的提倡者否认子句在程序维护上存在潜在的危险,并提出应给那些生成了这种风险的程序设计者一个不及格分数;我们认为标准名称消除了这种风险。
如果上面所建议的为减轻因限制使用“use”子句所带来影响的方法看上去需要太多输入,那么想想 Norman H. Cohen 所做的一个结论:“任何在程序输入上所节省下的时间,将数倍地在复审、调试和维护程序的过程中损失掉。”
最后,已证明在大型系统中无“use”子句可减少在符号表中的查找开销,从而缩短编译时间。
想了解更多有关 use 子句争议的读者可参阅以下资料:
D. Bryan, "Dear Ada," Ada Letters, 7, 1, January-February 1987, pp. 25-28.
J. P. Rosen, "In Defense of the Use Clause," Ada Letters, 7, 7, November-December 1987, pp. 77-81.
G. O. Mendal, "Three Reasons to Avoid the Use Clause," Ada Letters, 8, 1, January-February 1988, pp. 52-57.
R. Racine, "Why the Use Clause Is Beneficial," Ada Letters, 8, 3, May-June 1988, pp. 123-127.
N. H. Cohen, Ada as a Second Language, McGraw-Hill (1986), pp. 361-362.
M. Gauthier, Ada-Un Apprentissage, Dunod-Informatique, Paris (1989), pp. 368-370.]
有两种分解大型“逻辑”程序包的基本方法,从最初的设计阶段发展到几个较小的易于管理、编译、维护和理解的 Ada 库单元:
a)嵌套式分解
这种方法强调 Ada 子单元和/或子程序包的使用。主要的子程序、任务主体和内部程序包主体都被系统地分隔开了。该进程在这些子单元/子程序包内递归重复地运行。
b)平面分解
逻辑程序包被分解为一些用“with”子句互连的小程序包的网络,原始逻辑程序包很大程度上是一个重新导出外表程序(或者是一个不再存在的设计工件)。
每种方法都既有优点也有缺点。嵌套式的分解需要写的代码较少,命名较简单(许多标识不需要加前缀);此外至少在 Rational 环境中,结构在库映像中可见度很好,并且结构更容易转换(命令 Ada.Make_Separate、Ada.Make_Inline)。平面分解常使得重编译减少并且结构更好或更整洁(尤其是在子系统边界处);另外它也加强了重用。通过使用自动重编译工具和配置管理,它也令管理更加容易。但是平面结构通过“with”处理一些在分解中生成的低级程序包,带来了违反最初设计的较大风险。
子程序的嵌套级应限制在三级以内,程序包则应限制在二级以内;不要让程序包嵌套在子程序中。
package Level_1 is package Level_2 is package body Level_1 is procedure Level_2 is procedure Level_3 is
当出现以下情况之一时对嵌套单元(单独的主体)使用体存根:
程序包说明的声明部分所包含的声明应按如下顺序排列:
1) 程序包本身的重命名声明
2) 导入实体的重命名声明
3) “Use”子句
4) 命名数
5) 类型和子类型声明
6) 常量
7) 异常声明
8) 导出子程序说明
9) 嵌套程序包(如有)
10) 专用部分。
对于引入了几个主要类型的程序包,最好有几套相关声明:
5) A 的类型和子类型声明
6) 常量
7) 异常声明
8) A 上操作的导出子程序说明
5) B 的类型和子类型声明
6) 常量
7) 异常声明
8) B 上操作的导出子程序说明
等等。
当声明部分很大(>100 行)时,使用小的注释块来划分不同部分的界限。
程序包主体声明的声明部分所包含的声明应按如下顺序排列:
1) 重命名声明(用于导入实体)
2) “Use”子句
3) 命名数
4) 类型和子类型声明
5) 常量
6) 异常声明
7) 局部子程序说明
8) 局部子程序主体
9) 导出子程序主体
10) 嵌套程序包主体(如有)。
其他声明部分(例如在子程序主体、任务主体和块语句中)遵循相同的通用模式。
每个导入的库单元使用一个“with”子句。按照字母顺序排列“with”子句。如果一个使用“with”子句的单元上的“use”子句适当,则该子句应紧接上相应的“with”子句。请参阅以下编译指示 Elaborate。
不要依赖库单元的精化顺序来实现任何特殊效果。
只要满足 Ada Reference Manual [ISO87] 中阐述的一些很简单的规则,每一个 Ada 实施都可以自由选择一个策略来计算精化顺序。一些实施使用比其他程序更明智的策略(例如在相应的说明之后及早确立主体),而一些实施不使用这些策略(尤其是类属实例),从而会导致非常严重的可移植问题。
在程序精化中,有三个主要的导致声名狼藉的“精化前访问”错误的来源(通常会引起 Program_Error 异常):
task type T; type T_Ptr is access T; SomeT : T_Ptr := new T; -- Access before elaboration.
要避免将应用程序从一个 Ada 编译器移植到另一个编译器时产生问题,程序员应通过代码重构(并不总是可实现的)消除这些问题,或者通过编译指示 Elaborate 的方法使用以下策略来明确控制精化顺序:
在单元 Q 的上下文子句中,应将编译指示 Elaborate 运用于每个出现在“with”子句中的单元 P:
另外如果 P 导出了一个类型 T,并且类型 T 的对象的精化调用了一个 R 程序包中的函数,那么 Q 的上下文子句应包含:
with R; pragma Elaborate (R);
即使在 Q 中未直接引用 R!
实际上,说明程序包 P 应包含以下内容可能会更容易一些(但并不总是可能):
with R; pragma Elaborate (R);
程序包 Q 中必须只有:
with P; pragma Elaborate (P);
这样便通过传递提供了正确的精化顺序。
限制任务的使用。
任务是一个很强大的特性,但是它们的用法讲究。不明智地使用任务将导致很大的空间和时间开销。 对系统某些部分的小修改可能全面地威胁到一组任务的运行状况,产生饥饿和/或死锁。测试和调试任务程序很困难。因此任务的使用、位置和相互作用是一个项目级的决定。不能通过一种隐藏的方式使用任务,或者由无经验的程序员来编写任务。Ada 程序的任务模型应是可见的和可理解的。
除非可以获得并行硬件的有效支持,否则应仅在确实需要并行时才引入任务。周期性的活动或者超时的引入会表达依赖于时间的操作,或者表达依赖于一个如中断或外部消息到达等外部事件的操作。也需引入任务来对一些其他活动去耦,譬如:对公共资源的访问进行缓冲、排队、分派以及同步。
用一个 'Storage_Size 长度子句指定任务堆栈的大小。
在与要求集合使用长度子句的相同的原因和环境下(前文“访问类型”一节),当内存是一种宝贵资源时应说明任务的大小。要达到这一点,应始终声明已显式声明了类型的任务(因为长度子句只能用于一个类型)。可用一个函数调用来动态调整堆栈的大小。
注意:估计每个任务需要多大的堆栈可能非常困难。为辅助这一点,运行时系统可以利用一种“高水位标志”机制。
在任务主体中使用异常处理程序来避免或至少报告一个任务的无法解释的死亡。
不处理异常的任务常常悄悄死掉。只要可能,请尝试报告死亡性质,尤其是 Storage_Error。这可以很好地微调堆栈大小。请注意,这会要求将分配(原语 new)封装在一个可重新导出除 Storage_Error 之外的异常的子程序中。
在程序精化时创建任务。
在与要求集合在程序精化中分配的相同的原因和环境下(前文“访问类型”一节),整个应用程序任务结构应在程序启动时及早创建。与其让程序在几天之后死去,还不如因为内存耗尽而完全不启动它。
在后继规则中,服务任务和应用程序任务间有一些区别。服务任务是一些算法简单的、用于将应用程序相关任务“粘合”起来的小任务。服务任务(或中介任务)的例子有缓冲区、传送器、中继器、代理程序、监视器等,它们通常提供同步、去耦、缓冲和等待服务。而应用程序任务(正如其名称所传达的那样)更直接地与应用程序的主要功能相关联。
避免混合任务:应用任务应是纯粹的调用者,而服务任务应是纯粹的被调用者。
一个纯粹的被调用者是只包含接受语句或选择性等待并且没有入口调用的任务。
避免在入口调用图中出现循环。
这可以极大地降低死锁风险。如果不能完全避免,则至少应在系统处于稳定状态时避免循环。这两条规则也使结构更易于理解。
限制共享变量的使用。
尤其要小心隐藏的共享变量 - 举例来说,这些变量隐含在程序包主体内并可由对几个任务可见的原语访问。在需要同步对公共数据结构的访问而集合代价太大的极端情况下,可使用共享变量。检查编译指示 Shared 是否被有效支持。
限制使用异常中断语句。
异常中断语句是编程语言中公认的最不安全和最有害处的原语之一。它的无条件(并且几乎是异步的)终止任务的用法使判断给定任务结构的行为几乎成为不可能。但是也有极少数需要异常中断语句的情况。
例如:所提供的一些低级服务缺乏超时工具。唯一可引入超时的方法是,让某个辅助代理程序任务提供该服务,等待(用一个超时)代理程序的答复,如果在延时内未提供服务,则用异常中断杀死代理程序。
当可以证明只有中断者和被中断者会受到影响时,可以使用异常中断,例如当没有任何其他的任务可以调用被中断任务时。
限制使用延迟语句。
随意将一个任务挂起可能导致严重的调度问题,这种问题难于追踪和纠正。
限制使用 'Count、'Terminated 和 'Callable 属性。
'Count 属性只能作为一个大致说明,调度决策不应该基于该属性值是否为零而作出,因为从计算该属性值到运用该属性值这段时间里,等待中的任务的实际数目可能发生了变化。
使用有条件的入口调用(或者可以接受的等价构造)来可靠地检查是否缺少等待中的任务。
select The_Task.Some_Entry; else -- do something else end select;
这要好于以下代码:
if The_Task.Some_Entry'Count > 0 then The_Task.Some_Entry; else -- do something else end if;
'Terminated 属性只有为“True”时才有意义,'Callable 只有为“False”时才有意义,因而在很大程度上限制了它们的有用性。它们不应用于在系统关闭时提供任务间同步。
限制优先级的使用。
Ada 中的优先级对调度的影响有限。特别是那些等待入口的任务的优先级是不会被考虑用于对入口队列排序,或者用于在选择性等待语句中选择待服务入口的。这可能产生优先级倒置问题(请参阅 [GOO88])。优先级只被调度程序用来在待运行任务中选取下一个要运行的任务。因为存在优先级倒置的风险,所以对于互斥不要依赖优先级。
通过使用入口族,可以将入口队列分成几个子队列,这样常常可以引入一个关于紧急程度的明确概念。
如果优先级不是必要的,就不要给任何任务指定优先级。
一旦给一个任务分配了优先级,则要给应用程序中的所有任务都分配优先级。
需要这条规则是因为任务的优先级在无编译指示 Priority 的情况下是未定义的。
出于可移植性考虑,让优先级级别的数目尽量小。
子类型 System.Priority 的范围由实施定义,经验证明实际可行的范围随系统的不同变化很大。另外最好集中定义优先级并给出名称和定义,而不是在所有的任务中使用整型字面值。存在这样一个中心的 System_Priorities 程序包有助于可移植性,并与前述的规则一起使所有任务说明的定位变得容易。
为避免在循环任务中漂移,在编写延迟语句时要考虑处理时间、开销和任务优先权:
Next_Time := Calendar.Clock; loop -- Do the job. Next_Time := Next_Time + Period; delay Next_Time - Clock; end loop;
注意 Next_Time - Clock 可能为负,这说明循环任务运行滞后。可以去掉一个周期。
为保证可调度性,按 Rate Monotonic 调度算法给循环任务分配优先级,即频率最高的任务优先级最高。(更多详细信息请参阅 [SHA90]。)
将较高的优先级分配给速度很快的中继服务器:监视器、缓冲区。
但是要确保这些服务器不会因为要集合其他任务而将自己阻塞。在代码中说明这个优先级以便在程序维护时重视这一点。
为使“抖动”所产生的效应减至最小,请遵循给输入样本或输出数据加时间戳记的方法,而不是遵循这段时间本身。
避免忙等待(轮询)。
确保任务在有选择、入口调用或被延迟的情况下等待,而不是拼命检查要做的事。
对每个集合,要确保至少有一方在等待,并且只有一方具有条件入口调用、计时入口调用或等待。
否则,尤其在循环中,代码存在进入竞争状态的风险,与忙等待状态的结果极为相似。如果未能很好地使用优先级可能使这种情况恶化。
封装任务时,要保证让它们的一些特性高度可见。
如果在子程序中隐藏了入口调用,要确保阅读这些子程序说明的读者能够意识到,调用这个子程序有可能阻塞。此外要说明等待是否有界;如果有,给出对上界的估计。使用命名约定来说明可能的等待(前文中“子程序”一节)。
如果程序包的精化、子程序的调用或类属单元的实例化激活了一个任务,应让这一点对客户端可见:
package Mailbox_Io is -- This package elaborates an internal Control task -- that synchronizes all access to the external -- mailbox procedure Read_Or_Wait (Name: Mailbox.Name; Mbox: in out Mailbox.Object); -- -- Blocking (unbounded wait).
在一个选择性等待语句中,不要依赖于入口选择的任何特定顺序。
如果要公平地选择在入口排队的任务,则可通过无等待语句按需要的顺序来明确地检查队列,然后在所有入口上等待。不要使用 'Count。
对于同一声明部分中详述的任务,不要依赖任何特定的激活顺序。
如果要寻找到一个特定的启动顺序,应通过与特殊启动入口集合来实现。
实施任务,直到正常终止。
除非应用程序的性质要求任务一旦被激活就永久运行,否则任务应该通过正常完成或者通过终止选择语句来终止。这对于宿主是库级程序包的任务来说,可能是不可行的,因为 Ada Reference Manual 未说明在何种条件下它们应该终止。
如果依赖于宿主的结构不支持干净地终止,那么任务应提供并且等待那些在系统关闭时调用的特殊关闭入口。
通常的理念是,只对错误使用异常处理:逻辑和编程错误、配置错误、被破坏的数据、资源耗尽等等。通常的规则是,系统在正常状态下以及无重载或硬件失效状态下,不应产生任何异常。
使用异常来处理逻辑和编程错误、配置错误、被破坏的数据和资源耗尽。尽早(包括在发生时刻)使用适当的日志机制来报告异常。
使从一个给定抽象中导出的异常个数尽量少。
在大型系统中,需要在每个级别处理大量异常从而使代码很难读取和维护。有时异常处理会阻碍正常的处理。
有以下几种方式可尽量减少异常的数量:
不要传播未在设计中指定的异常。
除非被捕捉到的异常重新生成,否则应避免在异常处理程序中使用 when others 选择语句。
这样通过不介入无法在本级别处理的异常,实现某种程度的本地管理:
exception when others => if Io.Is_Open (Local_File) then Io.Close (Local_File); end if; raise; end;
另一个可使用 when others 选择语句的地方是任务主体的末尾。
对于经常发生的可预计事件不要使用异常。
用异常来表达并非一定是错误的状态有几个不便之处:
例如,不要将异常用作函数返回的某种形式的附加值(象搜索中的 Value_Not_Found);使用带“out”参数的过程,或者引入一个含义为 Not_Found 的特殊值,或者用判别式 Not_Found 将返回类型封装在一个记录中。
不要使用异常来实施控制结构。
这是前面规则的一个特例:异常不应作为“goto”语句的一种形式来使用。
捕获预定义异常时,将处理程序放在一个很小的框架中,该框架中包含了包含生成该异常的构造。
象 Constraint_Error、Storage_Error 等的预定义异常可在许多地方产生。如果因为某种特定原因要捕获某个此类异常,应尽可能将处理程序限制在作用域范围内:
begin Ptr := new Subscriber.Object; exception when Storage_Error => raise Subscriber.Collection_Overflow; end;
在函数中用一个“return”语句或者“raise”语句来终止异常处理程序。否则将在调用程序内产生 Program_Error 异常。
限制检查的禁用。
对于当前的 Ada 编译器,通过禁用检查来减少代码长度和提高性能的可能性不大。因此应使检查禁用仅限于(通过评估)被认为是性能瓶颈的数量非常有限的代码段上;它决不能广泛应用于整个系统。
必然的结论是,不要只是为了后来某人会决定禁用检查这种不大可能出现的情况而增加额外的明确范围和判别式检测。 依靠 Ada 内置的约束检查工具。
不要将异常传播到在其声明的作用域之外。
这会使客户端代码无法明确地处理异常,除非使用 when others 选择语句,但它可能不够具体。
这条规则的一个必然结论是:当通过派生重新导出一个类型时,要考虑重新导出派生子程序可能产生的异常,例如通过重命名。否则,客户端将不得不通过“with”子句处理原始的定义程序包。
一定要将 Numeric_Error 和 Constraint_Error 一起处理。
Ada 设计组已决定所有可能产生 Numeric_Error 的情况都应生成 Constraint_Error。
确保状态码有一个正确值。
当使用由子程序返回的状态码作为“out”参数时,一定要确保“out”参数赋了值,这可以通过将赋值语句作为子程序主体的第一个可执行语句来实现。 缺省情况下,系统化地将所有状态设置为“成功”或“失败”。 考虑子程序的所有可能出口,包括异常处理程序。
本地执行安全检查;不要期望客户机会这样做。
也就是说,如果给定错误输入时子程序可能生成错误输出,则应在子程序中加入以受控方式检测和报告无效输入的代码。不要依赖注释来告知客户机传递正确的值。如果没有检测到那些无效参数,则几乎可以断定该注释迟早会被忽略掉,从而导致难以调试的错误。
进一步信息请参阅 [KR90b]。
这一部分讨论先验的、不可移植的 Ada 语言特征。它们在 Reference Manual for the Ada Programming Language [ISO87] 的第 13 章中定义,特定于编译器的特征在 Ada 编译器销售商提供的“Appendix F”中说明。
仔细研读 Ada Reference Manual 的 Appendix F(并进行一些小测试以保证真正理解)。
限制表示子句的使用。
在不同的实施中,表示子句并不是总得到支持。它们的用法中有许多的陷阱。因此不应在系统上随意使用它们。
表示子句应该执行以下操作:
表示子句可在如下情形下避免:
示例:
替换:
type Foo is (Bla, Bli, Blu, Blo); for Foo use (Bla => 1, Bli =>3, Blu => 4, Blo => 5);
type Foo is (Invalid_0, Bla, Invalid_2, Bli, Blu, Blo);
将具有表示子句的类型分到一些程序包,这些程序包被明确识别出含有依赖于实施的代码。
在记录布局中,不要假定一个特定的顺序。
在一个记录表示子句中,一定要说明所有判别式的位置,并一定要在说明变量内的任何组件之前说明。
避免对齐子句。
相信编译器会将工作完成得很好;它了解目标对齐约束。程序员如果使用对齐子句,可能会导致后来发生对齐冲突。
注意在无约束组合类型中存在由编译器生成的字段:
在记录中:动态字段的偏移量、变体子句索引、约束位等等
在数组中:预测向量。
关于编译器的详细信息请参阅 Appendix F。不必遵循 Ada Reference Manual [ISO87] 第 13 章的内容。
限制 Unchecked_Conversion 的使用。
不同 Ada 编译器对 Unchecked_Conversion 的支持程度变化很大,且确切行为也可能略微不同,尤其是应用于组合类型和访问类型时。
在一个 Unchecked_Conversion 的实例中,要保证源类型和目标类型都受约束并且具有相同的大小。
这是实现有限的可移植性和避免实施添加信息(如预测向量)产生问题的唯一方法。一种保证使两种类型大小相同的方法是,将它们“打包”到一个具有记录表示子句的记录类型中。
一种使类型受约束的方式是在一个已预先计算出约束的“外表”函数中进行实例化。
不要将 Unchecked_Conversion 应用于存取值或者任务。
不仅并非所有系统(如 Rational 本机编译器)都支持这一点,而且也不应假定:
在这里我们概括一下要注意的最重要事项:
本文档从 Ada Guidelines: Recommendations for Designer and Programmers,Application Note #15,Rev. 1.1,Rational,Santa Clara,Ca.,1990. [KR90a] 直接派生出来。但是详细内容参考了许多不同的材料。
BAR88 B. Bardin & Ch. Thompson, "Composable Ada Software Components and the Re-export Paradigm", Ada Letters, VIII, 1, Jan.-Feb. 1988, p.58-79.
BOO87 E. G. Booch, Software Components with Ada, Benjamin/Cummings (1987)
BOO91 Grady Booch: Object-Oriented Design with Applications, Benjamin-Cummings Pub. Co., Redwood City, California, 1991, 580p.
BRY87 D. Bryan, "Dear Ada," Ada Letters, 7, 1, January-February 1987, pp. 25-28.
COH86 N. H. Cohen, Ada as a Second Language, McGraw-Hill (1986), pp. 361-362.
EHR89 D. H. Ehrenfried, Tips for the Use of the Ada Language, Application Note #1, Rational, Santa Clara, Ca., 1987.
GAU89 M. Gauthier, Ada-Un Apprentissage, Dunod-Informatique, Paris (1989), pp. 368-370.
GOO88John B. Goodenough and Lui Sha: "The Priority Ceiling Protocol," special issue of Ada Letters, Vol., Fall 1988, pp. 20-31.
HIR92 M. Hirasuna, "Using Inheritance and Polymorphism with Ada in Government Sponsored Contracts", Ada Letters, XII, 2, March/April 1992, p.43-56.
ISO87 Reference Manual for the Ada Programming Language, International Standard ISO 8652:1987.
KR90a Ph. Kruchten, Ada Guidelines: Recommendations for Designer and Programmers, Application Note #15, Rev. 1.1, Rational, Santa Clara, Ca., 1990.
KR90b Ph. Kruchten, "Error-Handling in Large, Object-Based Ada Systems," Ada Letters, Vol. X, No. 7, (Sept. 1990), pp. 91-103.
MCO93 Steve McConnell, Code Complete-A Practical Handbook of Software Construction, Microsoft® Press, Redmond, WA, 1993, 857p.
MEN88 G. O. Mendal, "Three Reasons to Avoid the Use Clause," Ada Letters, 8, 1, January-February 1988, pp. 52-57.
PER88 E. Perez, "Simulating Inheritance with Ada", Ada letters, VIII, 5, Sept.-Oct. 1988, p. 37-46.
PLO92 E. Ploedereder, "How to program in Ada 9X, Using Ada 83", Ada Letters, XII, 6, November 1992, pp. 50-58.
RAC88 R. Racine, "Why the Use Clause Is Beneficial," Ada Letters, 8, 3, May-June 1988, pp. 123-127.
RAD85 T. P. Bowen, G. B. Wigle & J. T. Tsai, Specification of Software Quality Attributes, Boeing Aerospace Company, Rome Air Development Center, Technical Report RADC-TR-85-37 (3 volumes).
ROS87 J. P. Rosen, "In Defense of the Use Clause," Ada Letters, 7, 7, November-December 1987, pp. 77-81.
SEI72 E. Seidewitz, "Object-Oriented Programming with Mixins in Ada", Ada Letters, XII, 2, March/April 1992, p.57-61.
SHA90 Lui Sha and John B. Goodenough: "Real-Time Scheduling Theory and Ada," Computer, Vol. 23, #4 (April 1990), pp. 53-62.)
SPC89 Software Productivity Consortium: Ada Quality and Style-Guidelines for the Professional Programmer, Van Nostrand Reinhold (1989)
TAY92 W. Taylor, Ada 9X Compatibility Guide, Version 0.4, Transition Technology Ltd., Cwmbran, Gwent, U.K., Nov. 1992.
WIC89 B. Wichman: Insecurities in the Ada Programming Language, Report DITC137/89, National Physical Laboratory (UK), January 1989.
本文档中使用的大多数术语在 Reference Manual for the Ada Programming Language,[ISO87] 的 Appendix D 中定义。 其他术语定义如下:
环境(Environment):使用中的 Ada 软件开发环境。
可变的(Mutable):一种记录的属性,这种记录的判别式具有缺省值;一个可变类型的对象可被赋予该种类型的任何值,甚至是使判别式改变从而使结构也改变的值。
库开关(Library switch):在 Rational 环境中应用于整个程序库的一个编译选项。
模型世界(Model world):在 Rational 环境中用来捕获项目范围统一的库开关设置的特殊库。
外表程序(Skin):一种主体只作为中继的子程序。理想情况下它只含有一个语句:对另一个子程序的调用,其中被调用子程序的一组参数与它相同,或是可与它的参数进行相互转换。
ADL:作为设计语言的 Ada;指的是使用 Ada 来表述设计的各个方面的方法;也叫 PDL 或者程序设计语言。
PDL:程序设计语言。