跳转到内容

Ada 风格指南/可读性

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

源代码展示 · 程序结构

本章建议使用 Ada 特性来使代码更易于阅读和理解。关于注释和可读性,有很多误解。真正的可读性更多地取决于命名和代码结构,而不是注释。注释行和代码行的数量一样多并不意味着可读性;更可能表明作者没有理解需要传达什么。

源代码中的拼写约定包括大小写规则以及下划线、数字和缩写的用法。如果您始终如一地遵循这些约定,则生成的代码将更清晰、更易读。

下划线的用法

[编辑 | 编辑源代码]
  • 使用下划线分隔复合名称中的单词。
Miles_Per_Hour
Entry_Value

当标识符包含多个单词时,如果单词之间用下划线分隔,则更易于阅读。事实上,英语中也有用连字符或空格分隔复合词的先例。除了提高代码可读性之外,如果在名称中使用下划线,代码格式化程序就可以更好地控制大小写更改。请参见指南 3.1.3。

  • 以一致的方式表示数字。
  • 以适合问题的基数表示字面量。
  • 使用下划线分隔数字,就像在普通文本中使用逗号或句号(或非十进制基数的空格)一样。
  • 使用科学计数法时,始终使 E 为大写或小写。
  • 在备用基数中,以全大写或全小写表示字母字符。

实例化

[编辑 | 编辑源代码]
  • 十进制和八进制数字从基数点左侧开始每三位一组,从基数点右侧开始每五位一组。
  • 科学记数法中的 E 始终大写。
  • 大于 10 进制的数字,用大写字母表示。
  • 十六进制数字从基数点两侧开始每四位一组。

示例

[edit | edit source]
type Maximum_Samples     is range          1 ..  1_000_000;
type Legal_Hex_Address   is range   16#0000# ..   16#FFFF#;
type Legal_Octal_Address is range 8#000_000# .. 8#777_777#;

Avogadro_Number : constant := 6.02216_9E+23;

要表示 1/3 作为常量,使用

One_Third : constant := 1.0 / 3.0;

避免使用

One_Third_As_Decimal_Approximation : constant := 0.33333_33333_3333;

One_Third_Base_3 : constant := 3#0.1#;

理由

[edit | edit source]

一致地使用大写或小写有助于扫描数字。下划线用于将数字部分分组为熟悉的模式。与日常使用环境中的通用用法保持一致是可读性的重要组成部分。

notes

[edit | edit source]

如果一个有理分数在某个基数下表示为有限小数而不是无限循环小数,如上例中的 3#0.1#,则在转换为机器基数后,其精度可能会有所提高。(对于像本例中这样的命名数字来说,这是错误的——它们必须精确计算。)

Capitalization

[edit | edit source]

指南

[edit | edit source]
  • 使保留字和其他程序元素在视觉上彼此区分。

实例化

[edit | edit source]
  • 所有保留字(用作保留字时)用小写。
  • 所有其他标识符用混合大小写,每个单词以大写字母开头,并用下划线分隔。
  • 缩略语和首字母缩略词用大写字母(参见自动化说明)。

示例

[edit | edit source]
...

type Second_Of_Day      is range 0 .. 86_400;
type Noon_Relative_Time is (Before_Noon, After_Noon, High_Noon);

subtype Morning   is Second_Of_Day range 0 .. 86_400 / 2 - 1;
subtype Afternoon is Second_Of_Day range Morning'Last + 2 .. 86_400;

...

Current_Time := Second_Of_Day(Calendar.Seconds(Calendar.Clock));
if Current_Time in Morning then
   Time_Of_Day := Before_Noon;
elsif Current_Time in Afternoon then
   Time_Of_Day := After_Noon;
else
   Time_Of_Day := High_Noon;
end if;

case Time_Of_Day is
   when Before_Noon =>   Get_Ready_For_Lunch;
   when High_Noon   =>   Eat_Lunch;
   when After_Noon  =>   Get_To_Work;
end case;

...

理由

[edit | edit source]

在视觉上区分保留字可以让你专注于程序结构本身,也可以帮助扫描特定标识符。

这里选择的实例化对于有经验的 Ada 程序员来说更易读,他们不需要保留字来突出显示。任何语言的初学者往往会发现保留字应该得到强调,以帮助他们更容易找到控制结构。因此,课堂上的讲师和介绍 Ada 语言的书籍可能会考虑使用另一种实例化。Ada 参考手册 (1995) 选择将所有保留字以粗体小写显示。

automation notes

[edit | edit source]

Ada 名称不区分大小写。因此,名称 max_limitMAX_LIMITMax_Limit 表示同一个对象或实体。一个好的代码格式化程序应该能够在不同风格之间自动转换,只要单词以下划线分隔。

正如指南 3.1.4 中建议的那样,缩写应在整个项目中使用。自动化工具应允许项目指定这些缩写并相应地格式化它们。

Abbreviations

[edit | edit source]

指南

[edit | edit source]
  • 不要在存在更短同义词的情况下,使用长单词的缩写作为标识符。
  • 使用一致的缩写策略。
  • 不要使用模棱两可的缩写。
  • 为了证明缩写的必要性,缩写必须比完整单词节省许多字符。
  • 使用在应用领域中被广泛接受的缩写。
  • 维护一个已接受缩写的列表,并且只使用该列表中的缩写。

示例

[edit | edit source]

使用

Time_Of_Receipt

而不是

Recd_Time or R_Time

但在通常处理符合军用标准的消息格式的应用程序中,DOD_STD_MSG_FMT 是对

Department_Of_Defense_Standard_Message_Format.

理由

[edit | edit source]

许多缩写都是模棱两可的,或者除非在上下文中理解,否则无法理解。例如,Temp 可以表示 temporary 或 temperature。因此,使用缩写时应谨慎选择。指南 8.1.2 中的理由更详细地讨论了上下文如何影响缩写的使用。

由于非常长的变量名可能会掩盖程序的结构,尤其是在嵌套很深(缩进)的控制结构中,因此尽量保持标识符简短且有意义是一个好主意。尽可能使用简短的非缩写名称。如果找不到任何简短的单词可以用作标识符,那么一个众所周知的、非模棱两可的缩写是次佳选择,尤其是在它来自整个项目中使用的标准缩写列表的情况下。

可以使用 renames 子句为完全限定名建立缩写格式。当一个非常长、完全限定的名称否则将在代码的本地化部分多次出现时,此功能非常有用(参见指南 5.7.2)。

项目中已接受的缩写列表为使用每个缩写提供了标准上下文。

Naming Conventions

[edit | edit source]

选择能说明对象或实体预期用途的名称。Ada 允许标识符为任何长度,只要标识符在一行中可以容纳,并且所有字符都有意义(包括下划线)。标识符是用于变量、常量、程序单元和其他程序实体的名称。

Names

[edit | edit source]

指南

[edit | edit source]
  • 选择尽可能自说明的名称。
  • 使用简短的同义词代替缩写(参见指南 3.1.4)。
  • 使用应用程序提供的名称,但不要使用晦涩的术语。
  • 避免使用相同名称来声明不同类型的标识符。

示例

[edit | edit source]

在树遍历器中,使用 Left 而不是 Left_Branch 就足以在给定上下文中传达完整含义。但是,使用 Time_Of_Day 而不是 TOD

数学公式通常使用单字母名称来表示变量。在数学方程中继续使用这种约定,这些方程可以回忆公式,例如

   A*(X**2) + B*X + C.

使用子包时,如果包、子单元和标识符名称选择不当,会导致子单元的可见性冲突。有关结果代码(相当晦涩)的示例,请参见理由 (1995, §8.1)。

理由

[edit | edit source]

遵循这些指南的程序可以更容易地理解。自说明名称需要更少的解释性注释。实证研究表明,如果变量名称不至于过长,则可以进一步提高理解力 (Schneiderman 1986, 7)。上下文和应用可以提供很大的帮助。数值实体的度量单位可以作为子类型名称的来源。

应尽量避免在不同的声明中使用相同的名称作为标识符,例如对象和子包。在看似不同的命名空间中过度使用标识符实际上会导致可见性冲突,如果封闭的程序单元旨在协同工作。

notes

[edit | edit source]

有关如何使用应用程序领域作为选择缩写的指南,请参见指南 8.1.2。

Subtype Names

[编辑 | 编辑源代码]
  • 使用单数、通用名词作为子类型标识符。
  • 选择描述子类型之一的值的标识符。
  • 考虑为定义可见访问类型、可见子范围或可见数组类型的子类型标识符使用后缀。
  • 对于私有类型,不要使用仅限于子类型标识符的标识符结构(例如,后缀)。
  • 不要使用预定义包中的子类型名称。
type Day is
   (Monday,    Tuesday,   Wednesday, Thursday,  Friday,
    Saturday,  Sunday);

type Day_Of_Month    is range      0 ..    31;
type Month_Number    is range      1 ..    12;
type Historical_Year is range -6_000 .. 2_500;

type Date is
   record
      Day   : Day_Of_Month;
      Month : Month_Number;
      Year  : Historical_Year;
   end record;

特别是,Day 应优先于 DaysDay_Type 使用。

标识符 Historical_Year 看起来可能很具体,但实际上很通用,其中形容词 historical 描述了范围约束。

------------------------------------------------------------------------
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;
------------------------------------------------------------------------

后缀 _Capacity_Range_Map 有助于定义上述子类型的目的,并避免搜索扇区、轨道和表面抽象的同义词。如果没有后缀,您将需要为每个抽象使用三个不同的名称,每个名称都简洁地描述后缀中命名的概念。此建议仅适用于某些可见子类型。例如,私有类型应该使用一个好的名称来反映正在表示的抽象。

当使用这种风格和建议的对象标识符风格时,程序代码更类似于英语(参见指南 3.2.3)。此外,这种风格与语言的预定义标识符的名称一致。它们没有被命名为 IntegersBooleansInteger_TypeBoolean_Type

但是,使用预定义包中的子类型名称肯定会让程序员在没有包限定的情况下看到该子类型时感到困惑。

本样式指南尝试在使用“类型”和“子类型”名称方面与 Ada 参考手册 (1995) 保持一致。一般来说,“类型”指的是抽象概念,如类型声明,而“子类型”指的是在实际声明中给该抽象概念的名称。因此,在 Ada 83(Ada 参考手册 1983)中被称为类型名称的现在被称为子类型名称。

对象名称

[编辑 | 编辑源代码]
  • 对布尔对象使用谓词子句或形容词。
  • 使用单数、具体名词作为对象标识符。
  • 选择描述对象在执行期间的值的标识符。
  • 使用单数、通用名词作为记录组件的标识符。

非布尔对象

Today           : Day;
Yesterday       : Day;
Retirement_Date : Date;

布尔对象

User_Is_Available : Boolean;        -- predicate clause
List_Is_Empty     : Boolean;        -- predicate clause
Empty             : Boolean;        -- adjective
Bright            : Boolean;        -- adjective

对对象使用具体名词为理解对象的值建立了上下文,该值是子类型名称描述的通用值之一(参见指南 3.2.2)。使用这种风格,对象声明变得非常类似于英语。例如,上面的第一个声明被解读为“今天是一天”。

对记录组件使用通用名词,而不是具体名词,因为记录对象的名称将提供理解组件的上下文。因此,以下组件被理解为“退休年份”。

Retirement_Date.Year

遵循将对象类型和词性联系起来的约定使代码更像文本。例如,由于选择了名称,以下代码段不需要注释。

if List_Is_Empty then
   Number_Of_Elements := 0;
else
   Number_Of_Elements := Length_Of_List;
end if;

如果难以找到一个描述程序整个执行期间对象值的具体名词,那么该对象可能在执行多个目的。在这种情况下,应该使用多个对象。

标记类型和关联包的命名

[编辑 | 编辑源代码]
  • 对标记类型和关联包使用一致的命名约定。

实例化

[编辑 | 编辑源代码]

命名约定引发“宗教战争”;因此,提供了两种不同的实例化。第一个实例化集成了面向对象功能的使用。除了两个特殊情况外,它对声明应用相同的命名约定,无论它们是否使用面向对象功能。

  • 以与子类型名称相同的方式命名标记类型(参见指南 3.2.2)。
  • 对导出抽象的包使用前缀 Abstract_,您打算为该抽象提供多个实现(参见指南 9.2.4)。
  • 对提供可以“混合”到核心抽象中的功能单元的包使用后缀 _Mixin

第二个实例化通过特殊名称或后缀突出显示面向对象功能的使用。

  • 以它们所代表的对象命名类包,不带后缀(Rosen 1995)。
  • 以它们所代表的方面命名混合包,附加后缀 _Facet(Rosen 1995)。
  • 将主要标记类型命名为 Instance(Rosen 1995)。
  • 在特定类型的声明之后,为相应的类范围类型使用一个名为 Class 的子类型(Rosen 1995)。

以下来自 Rationale (1995, §§4.4.4 和 4.6.2) 的两部分示例应用了第一个实例化的命名约定。

对于此示例的第一部分,假设类型 Set_Element 在其他地方声明。

package Abstract_Sets is

   type Set is abstract tagged private;

   -- empty set
   function Empty return Set is abstract;

   -- build set with 1 element
   function Unit (Element: Set_Element) return Set is abstract;

   -- union of two sets
   function Union (Left, Right: Set) return Set is abstract;

   -- intersection of two sets
   function Intersection (Left, Right: Set) return Set is abstract;

   -- remove an element from a set
   procedure Take (From    : in out Set;
                   Element :    out set_Element) is abstract;

   Element_Too_Large : exception;
private
   type Set is abstract tagged null record;
end Abstract_Sets;

with Abstract_Sets;
package Bit_Vector_Sets is   -- one implementation of set abstraction

   type Bit_Set is new Abstract_Sets.Set with private;
   ...
private
   Bit_Set_Size : constant := 64;
   type Bit_Vector is ...
   type Bit_Set is new Abstract_Sets.Set with
      record
         Data : Bit_Vector;
      end record;
end Bit_Vector_Sets;

with Abstract_Sets;
package Sparse_Sets  -- alternate implementation of set abstraction

   type Sparse_Set is new Abstract_Sets.Set with private;
   ...
private
   ...
end Bit_Vector_Sets;

此示例的第二部分将命名约定应用于支持窗口系统的混合包。

-- assume you have type Basic_Window is tagged limited private;

generic
   type Some_Window is abstract new Basic_Window with private;
package Label_Mixin is 
   type Window_With_Label is abstract new Some_Window with private;
   ...
private
   ...
end Label_Mixin;

generic
   type Some_Window is abstract new Basic_Window with private;
package Border_Mixin is 
   type Window_With_Label is abstract new Some_Window with private;
   ...
private
   ...
end Border_Mixin;

以下示例应用了第二个实例化的命名约定,如 Rosen (1995) 中所述。

package Shape is
   subtype Side_Count is range 0 .. 100;
   type Instance (Sides: Side_Count) is tagged private;
   subtype Class is Instance'Class;
   . . .
   -- operations on Shape.Instance
private
   . . .
end Shape;

with Shape; use Shape;
package Line is
   type Instance is new Shape.Instance with private;
   subtype Class is Instance'Class;
   . . .
   -- Overridden or new operations
private
   . . .
end Line;

with Shape; use Shape;
generic
   type Origin is new Shape.Instance;
package With_Color_Facet is
   type Instance is new Origin with private;
   subtype Class is Instance'Class;
   -- operations for colored shapes
private
   . . .
end With_Color_Facet;

with Line; use Line;
with With_Color_Facet;
package Colored_Line is new With_Color_Facet (Line.Instance);

示例声明可能如下所示。

Red_Line : Colored_Line.Instance;

procedure Draw (What : Shape.Instance);

无论您使用完整名称还是 use 子句,上述方案都有效。只要您对所有特定类型(即 type Instance)和类范围类型使用相同的名称,非限定名称将始终相互隐藏。因此,编译器会要求您使用完整名称限定来解决 use 子句引入的歧义(Rosen 1995)。

您需要使用一致且可读的命名方案,并传达抽象的意图。理想情况下,命名方案在处理使用标记类型创建类的方式方面应该是统一的。但是,如果命名约定过于严格,您将编写从可读性角度来看显得生硬的代码片段。通过对通过派生和通用混合进行类型扩展使用类似的命名约定(另请参见指南 9.5.1),您可以实现对象和过程的可读声明。

notes

[edit | edit source]

用于类的命名约定在面向对象抽象和其他类型的抽象之间划清了界限。鉴于工程师已经在 Ada 83(Ada 参考手册 1983)中定义了抽象数据类型超过 10 年,您可能不希望仅仅为了使用类型扩展而更改命名约定。您必须考虑在程序中整体使用抽象时,调用继承的用途有多重要。如果您更倾向于强调抽象,而不是实现抽象所使用的机制(即继承、类型扩展和多态性),您可能不想强制执行如此严格的命名约定。您不会通过有利于从没有继承开发的抽象到有继承开发的抽象的命名约定更平滑过渡来妨碍质量。

如果您选择一个突出显示面向对象功能的使用的命名约定,然后决定将声明更改为不使用面向对象功能的声明,则更改可能很昂贵。您必须自然地更改所有名称的出现,并且必须小心不要在更新名称时引入错误。如果您选择一个禁止使用后缀或前缀来表征声明的命名约定,那么您将失去传达声明项目的预期用途的机会。

程序单元名称

[edit | edit source]

指南

[edit | edit source]
  • 对过程和条目使用动作动词。
  • 对布尔函数使用谓词子句。
  • 对非布尔函数使用名词。
  • 为包提供暗示比子程序更高级别组织的名称。通常,这些是描述所提供抽象的名词短语。
  • 为任务提供暗示活动实体的名称。
  • 对受保护单元使用描述正在保护的数据的名词。
  • 考虑将泛型子程序命名为非泛型子程序。
  • 考虑将泛型包命名为非泛型包。
  • 使泛型名称比实例化名称更通用。

示例

[edit | edit source]

以下是构成 Ada 程序的元素的示例名称

示例过程名称

procedure Get_Next_Token          -- get is a transitive verb
procedure Create                  -- create is a transitive verb

用于布尔值函数的示例函数名称

function Is_Last_Item             -- predicate clause
function Is_Empty                 -- predicate clause

用于非布尔值函数的示例函数名称

function Successor                -- common noun
function Length                   -- attribute
function Top                      -- component

示例包名称

package Terminals is               -- common noun
package Text_Routines is           -- common noun

示例受保护对象

protected Current_Location is      -- data being protected
protected type Guardian is         -- noun implying protection

示例任务名称

task Terminal_Resource_Manager is  -- common noun that shows action

以下示例代码片段展示了使用词性命名约定带来的清晰度

Get_Next_Token(Current_Token);

case Current_Token is
   when Identifier =>         Process_Identifier;
   when Numeric    =>         Process_Numeric;
end case;  -- Current_Token

if Is_Empty(Current_List) then
   Number_Of_Elements := 0;
else
   Number_Of_Elements := Length(Current_List);
end if;

当包及其子程序一起命名时,生成的代码非常具有描述性

if Stack.Is_Empty(Current_List) then
   Current_Token := Stack.Top(Current_List);
end if;

理由

[edit | edit source]

使用这些命名约定创建易于理解的代码,这些代码读起来很像自然语言。当对动作(如子程序)使用动词,对对象(如子程序操作的数据)使用名词时,代码更容易阅读和理解。这模拟了读者已经熟悉的交流媒介。在程序的各个部分模拟现实生活的情况时,使用这些约定减少了阅读和理解程序所涉及的翻译步骤。从某种意义上说,您选择的名称反映了从计算机硬件到应用程序需求的抽象级别。

另请参见指南 3.2.4,了解在与标记类型关联的包中使用专用后缀。

notes

[edit | edit source]

目前在使用任务条目时存在一些相互矛盾的约定。一些程序员和设计人员主张对任务条目使用与子程序相同的约定进行命名,以模糊任务参与的事实。他们的理由是,如果任务被重新实现为包,反之亦然,则名称不必更改。另一些人更喜欢尽可能明确地说明任务条目的存在,以确保可以识别具有其假定开销的任务的存在。项目特定的优先级可能有助于在这些约定之间进行选择。

常量和命名数字

[edit | edit source]

指南

[edit | edit source]
  • 在符号值可以提高可读性的情况下使用符号值而不是文字。
  • 如果值在多个地方出现并且可能需要更改,则使用符号值而不是文字。
  • 对数学常量 Pi 和 e 使用预定义常量 Ada.Numerics.Pi 和 Ada.Numerics.e。
  • 对常数值使用常量而不是变量。
  • 当值特定于类型或当值必须是静态时使用常量。
  • 尽可能使用命名数字而不是常量。
  • 使用命名数字替换类型或上下文真正通用的数字文字。
  • 对在细化后其值无法更改的对象使用常量(United Technologies 1987)。
  • 通过使用静态表达式定义它们来显示符号值之间的关系。
  • 使用线性无关的文字集。
  • 尽可能使用 'First 和 'Last 等属性而不是文字。

示例

[edit | edit source]
3.14159_26535_89793                                 -- literal
Max_Entries : constant Integer       := 400;        -- constant
Avogadros_Number  : constant := 6.022137 * 10**23;  -- named number
Avogadros_Number / 2                                -- static expression
Avogadros_Number                                    -- symbolic value

Pi 声明为命名数字(假设在Ada 参考手册 1995,第 A.5 节[注释] 中为预定义包 Ada.Numerics 提供 with 子句)允许它在下面的赋值语句中以符号方式引用

Area :=       Pi * Radius**2;       -- if radius is known.

而不是

Area := 3.14159 * Radius**2;        -- Needs explanatory comment

此外,Ada.Characters.Latin_1.BelCharacter'Val(8#007#) 更具表现力。

通过使用其他常量和命名数字,可以提高常量和命名数字声明的清晰度。例如

Bytes_Per_Page   : constant := 512;
Pages_Per_Buffer : constant := 10;
Buffer_Size      : constant := Pages_Per_Buffer * Bytes_Per_Page;

比以下内容更具自解释性,更容易维护

Buffer_Size : constant := 5_120;   -- ten pages

以下文字应该是常量

if New_Character  = '$' then  -- "constant" that may change
...
if Current_Column = 7 then    -- "constant" that may change

理由

[edit | edit source]

使用标识符而不是文字使表达式的目的明确,减少了对注释的需求。由数字文字表达式组成的常量声明更安全,因为它们不需要手动计算。它们也比单个数字文字更具启发性,因为有更多机会嵌入解释性名称。通过在定义新常量的静态表达式中使用其他相关常量,可以进一步提高常量声明的清晰度。这并不影响效率,因为命名数字的静态表达式在编译时计算。

常量具有类型。命名数字只能是通用类型:universal_integer 或 universal_real。常量强制执行强类型,而命名数字或文字则不强制执行。命名数字允许编译器生成比常量更有效的代码,并在编译时执行更完整的错误检查。如果文字包含大量的数字(如上面的 Pi 示例),使用标识符可以减少按键错误。如果出现按键错误,则可以通过检查或在编译时更轻松地找到它们。

文字的独立性意味着使用的少量文字彼此不依赖,并且常量或命名值之间的任何关系都显示在静态表达式中。文字值的线性独立性具有以下属性:如果一个文字值发生变化,则所有依赖该文字的命名数字值将自动发生变化。

有关选择无参数函数与常量的更多指南,请参见指南 4.1.4。

notes

[edit | edit source]

在某些情况下,文字比名称更适合。要满足这种情况,必须满足以下条件

  • 文字必须在各自的上下文中具有自解释性,以便用符号值替换文字不会提高可读性。
  • 该值要么不可变,要么只在代码中的一个地方出现,因此用符号值替换文字不会提高可维护性。

例如,在以下众所周知的表达式中的文字既具有自解释性又不可变

   Fahrenheit := 32.0 + (9.0 * Celsius) / 5.0;

第二个例子是,在二分查找算法的上下文中,除以字面量 2 是不言自明的。并且,由于该值也与算法不可改变地相关,因此代码中字面量出现多次(例如,由于循环展开)也不重要。因此,使用如下所示的符号值既不会提高可读性,也不会提高可维护性。

   Binary_Search_Divisor : constant := 2;
  • 使用一个名称来指示异常代表的问题类型。
Invalid_Name: exception;
Stack_Overflow: exception;

根据异常检测的问题类型对其进行命名,可以提高代码的可读性。您应该尽可能精确地命名异常,以便代码维护人员了解异常可能抛出的原因。对于声明异常的包的客户端而言,一个命名良好的异常应该是意义重大的。

构造函数

[编辑 | 编辑源代码]
  • 在命名构造函数(在此意义上,指用于创建和/或初始化对象的运算符)时,包含类似 NewMakeCreate 的前缀。
  • 对于包含构造函数的子包,请使用反映其内容的名称。

实例化

[编辑 | 编辑源代码]
  • 将包含构造函数的子包命名为 <whatever>.Constructor
function Make_Square (Center : Cartesian_Coordinates; 
                      Side   : Positive)
  return Square;

在构造函数名称中包含类似 NewMakeCreate 的词语可以使其目的明确。您可能希望将 New 前缀的使用限制为返回访问值的构造函数,因为该前缀暗示了分配器的内部使用。

将所有构造函数放在一个子包中,即使它们返回访问值,也是一个有用的组织原则。

有关 Ada 构造函数使用的信息,请参阅指南 9.3.3。

源代码中的注释是一个有争议的话题。关于注释是否提高可读性,存在着正反两方面的论点。实际上,注释最大的问题是人们经常在修改相关源代码时没有更新注释,从而导致注释误导人。注释应该保留用于表达代码中无法表达的必要信息,并突出显示违反其中一项指南的必要理由。如果可能,源代码应该使用自解释的名称来命名对象和程序单元,并且应该使用简单易懂的程序结构,以便不需要额外的注释。选择(并输入)适当名称的额外努力,以及设计简洁易懂的程序结构所需的额外思考,是完全合理的。

使用注释来陈述代码的意图。提供代码概述的注释可以帮助维护程序员看到森林而不是树木。代码本身是详细的“如何”,不应该在注释中进行释义。

注释应该尽量减少。它们应该提供代码中无法表达的必要信息,强调代码结构,并提请注意对指南的故意和必要的违反。注释的存在要么是为了引起对示例中所阐述的真实问题的注意,要么是为了弥补示例程序中的不完整性。

维护程序员需要了解非连续代码片段的因果关系,才能对程序有一个全局的、或多或少完整的认识。他们通常从对代码部分的思维模拟中获得这种信息。注释应该足以支持此过程(Soloway 等人,1986)。

本节介绍了关于如何编写良好注释的一般指南。然后定义了几个不同的注释类别,并提供每个类别的使用指南。这些类别包括文件头、程序单元规范头、程序单元体头、数据注释、语句注释和标记注释。

一般注释

[编辑 | 编辑源代码]
  • 使代码尽可能清晰,以减少对注释的需求。
  • 切勿在注释中重复代码中已有的信息。
  • 在需要注释的地方,注释应该简洁完整。
  • 在注释中使用正确的语法和拼写。
  • 使注释在视觉上与代码区分开来。
  • 对注释进行结构化,以便工具可以自动提取信息。

编写良好的代码的结构和功能在没有注释的情况下也很清晰。无论注释如何,难以理解、维护或重用的代码都应该被改进,而不是进行解释。阅读代码本身是确保代码功能的唯一方法;因此,代码应该尽可能易读。

使用注释重复代码中的信息是一个不好的做法,原因有很多。首先,这是不必要的工作,会降低生产效率。其次,在修改代码时很难正确维护重复的信息。当对现有代码进行更改时,会对其进行编译和测试以确保其再次正确。但是,没有自动机制来确保注释已正确更新以反映更改。通常,注释中的重复信息在第一次代码更改时就会变得过时,并在软件的生命周期中一直保持过时状态。第三,当从单个子系统作者的有限视角来编写有关整个系统的注释时,注释从一开始就经常是错误的。

注释对于揭示从代码中难以或无法获得的信息是必要的。本书的后续章节包含此类注释的示例。完整且简洁地呈现所需信息。

注释的目的是帮助读者理解代码。拼写错误、语法错误、含糊不清或不完整的注释会破坏这个目的。如果值得添加注释,就应该正确添加以提高其有用性。

通过缩进注释、将注释分组到标题中或使用虚线突出显示注释,可以使注释在视觉上与代码区分开来,因为这使得代码更容易阅读。本书的后续章节将详细阐述这一点。

automation notes

[编辑 | 编辑源代码]

关于在注释中存储冗余信息的指南仅适用于手动生成的注释。有一些工具可以自动维护有关代码的信息(例如,调用单元、被调用单元、交叉引用信息、修订历史记录等),并将这些信息存储在与代码位于同一文件中的注释中。其他工具读取注释但不会更新它们,使用注释中的信息自动生成详细的设计文档和其他报告。

鼓励使用此类工具,这可能需要您构建头部注释,以便它们可以被自动提取和/或更新。注意,修改文件注释的工具只有在足够频繁地执行时才有用。自动生成的过时信息比手动生成的过时信息更危险,因为它更容易被读者信任。

配置管理工具可以更准确、更完整地维护修订历史记录。没有工具支持,工程师在进行更改后忘记更新修订历史记录的情况很常见。如果您的配置管理工具能够在源文件中维护修订历史记录作为注释,那么无论您可能不得不对修订历史记录的格式或位置做出什么妥协,都应该利用此功能。将完整的修订历史记录附加到文件末尾比将部分修订历史记录以良好的格式嵌入文件头中更好。

文件头

[编辑 | 编辑源代码]
  • 每个源文件中都需要一个文件头。
  • 将文件的所有权、责任和历史信息放在文件头中。

实例化

[编辑 | 编辑源代码]
  • 在文件头中添加版权声明。
  • 在文件头中添加作者姓名和部门。
  • 在文件头中添加修订历史记录,包括每次更改的摘要、日期和进行更改的人员姓名。
------------------------------------------------------------------------
--      Copyright (c) 1991, Software Productivity Consortium, Inc.
--      All rights reserved.
--
-- Author: J. Smith
-- Department:System Software Department
--
-- Revision History:
--   7/9/91 J. Smith
--     - Added function Size_Of to support queries of node sizes.
--     - Fixed bug in Set_Size which caused overlap of large nodes.
--   7/1/91 M. Jones
--     - Optimized clipping algorithm for speed.
--   6/25/91 J. Smith
--     - Original version.
------------------------------------------------------------------------

如果您想确保保护您对软件的权利,每个文件中都应该包含所有权信息。此外,为了提高可见度,它应该是文件中第一条信息。

为了方便未来的维护者,每个文件中都应该包含责任和修订历史记录信息;这是维护者最信任的头信息,因为它会积累。它不会演变。没有必要返回并修改作者姓名或文件的修订历史记录。随着代码的演变,修订历史记录应该更新以反映每次更改。最糟糕的是,它将是不完整的;它应该很少出错。此外,更改的数量和频率以及在单元历史记录中进行更改的不同人员的数量可以很好地指示实现相对于设计的完整性。

除了作者姓名之外,文件头中还应该包含有关如何找到原始作者的信息,以使维护者在出现问题时更容易找到作者。但是,诸如电话号码、邮件停止、办公室号码和计算机帐户用户名等详细信息过于不稳定,因此没有太大用处。最好记录作者编写代码时所在的部门。如果作者搬迁办公室、更换部门,甚至离开公司,此信息仍然有用,因为该部门可能会保留对代码原始版本的责任。

对于现代配置管理系统,显式地将版本历史记录作为头注释捕获可能是多余的。配置管理工具维护着更可靠、更一致(从内容角度来看)的更改历史记录。某些系统可以重新创建单元的早期版本。

程序单元规范头

[编辑 | 编辑源代码]
  • 在每个程序单元的规范中添加一个头。
  • 将程序单元用户所需的信息放在规范头中。
  • 不要重复规范头中已有的信息(单元名称除外)。
  • 解释单元的功能,而不是解释它如何或为何执行该功能。
  • 描述程序单元的完整接口,包括它可能引发的任何异常以及它可能产生的任何全局影响。
  • 不要包含有关单元如何适应封闭软件系统的信息。
  • 描述单元的性能(时间和空间)特征。

实例化

[编辑 | 编辑源代码]
  • 在头中添加程序单元的名称。
  • 简要解释程序单元的目的。
  • 对于包,描述可见子程序彼此之间的影响以及它们应如何一起使用。
  • 列出单元可能引发的所有异常。
  • 列出单元的所有全局影响。
  • 列出单元的前置条件和后置条件。
  • 列出由单元激活的隐藏任务。
  • 不要列出子程序的参数名称。
  • 不要只为了列出它们而列出包子程序的名称。
  • 不要列出单元使用的所有其他单元的名称。
  • 不要列出使用该单元的所有其他单元的名称。
     ------------------------------------------------------------------------
     -- AUTOLAYOUT
     --
     -- Purpose:
     --   This package computes positional information for nodes and arcs
     --   of a directed graph.  It encapsulates a layout algorithm which is
     --   designed to minimize the number of crossing arcs and to emphasize
     --   the primary direction of arc flow through the graph.
     --
     -- Effects:
     --   - The expected usage is:
     --     1. Call Define for each node and arc to define the graph.
     --     2. Call Layout to assign positions to all nodes and arcs.
     --     3. Call Position_Of for each node and arc to determine the
     --        assigned coordinate positions.
     --   - Layout can be called multiple times, and recomputes the
     --     positions of all currently defined nodes and arcs each time.
     --   - Once a node or arc has been defined, it remains defined until
     --     Clear is called to delete all nodes and arcs.
     --
     -- Performance:
     --   This package has been optimized for time, in preference to space.
     --   Layout times are on the order of N*log(N) where N is the number
     --   of nodes, but memory space is used inefficiently.
     ------------------------------------------------------------------------

     package Autolayout is

        ...

        ---------------------------------------------------------------------
        -- Define
        --
        -- Purpose:
        --   This procedure defines one node of the current graph.
        -- Exceptions:
        --   Node_Already_Defined
        ---------------------------------------------------------------------
        procedure Define
              (New_Node : in     Node);

        ---------------------------------------------------------------------
        -- Layout
        --
        -- Purpose:
        --   This procedure assigns coordinate positions to all defined
        --   nodes and arcs.
        -- Exceptions:
        --   None.
        ---------------------------------------------------------------------
        procedure Layout;

        ---------------------------------------------------------------------
        -- Position_Of
        --
        -- Purpose:
        --   This function returns the coordinate position of the
        --   specified node.  The default position (0,0) is returned if no
        --   position has been assigned yet.
        -- Exceptions:
        --   Node_Not_Defined
        ---------------------------------------------------------------------
        function Position_Of (Current : in     Node)
              return Position;

        ...

     end Autolayout;

程序单元规范上的头注释的目的是帮助用户了解如何使用程序单元。用户可以通过阅读程序单元规范和头了解使用该单元所需的一切。没有必要阅读程序单元的主体。因此,每个程序单元规范上都应该有一个头注释,并且每个头都应该包含规范本身未表达的所有使用信息。此类信息包括单元彼此之间的影响以及对共享资源的影响、引发的异常以及时间/空间特征。所有这些信息都无法从程序单元的 Ada 规范中确定。

当您在头中重复可以从规范中轻松获得的信息时,这些信息在维护过程中往往会变得不正确。例如,在描述过程时,不要特意列出所有参数名称、模式或子类型。此信息已从过程规范中获得。类似地,不要在头中列出包的所有子程序,除非这样做对于说明有关子程序的重要声明是必要的。

不要在头中包含程序单元用户不需要的信息。尤其是,不要包含有关程序单元如何执行其功能或使用特定算法的原因的信息。此信息应隐藏在程序单元的主体中,以保留单元定义的抽象。如果用户知道这些细节并根据这些信息做出决策,那么当这些信息稍后发生更改时,代码可能会受到影响。

在描述单元的目的时,避免提及封闭软件系统的其他部分。最好说“该单元执行……”而不是说“该单元被 Xyz 调用以执行……”。单元应该以一种不知道或不在乎哪个单元正在调用的方式编写。这使单元更加通用且可重用。此外,有关其他单元的信息在维护过程中很可能变得过时和不正确。

包含有关单元性能(时间和空间)特征的信息。Ada 规范中没有包含许多这些信息,但用户需要它们。为了将单元集成到系统中,用户需要了解单元的资源使用情况(CPU、内存等)。需要注意的是,当子程序调用导致在包主体中隐藏的任务激活时,该任务可能在子程序结束之后继续消耗资源。

一些项目已将大多数注释推迟到程序单元的末尾,而不是放在开头。他们的理由是程序单元只编写一次,但阅读多次,并且长的头注释会使规范的开头难以找到。

异常

[edit | edit source]

当一组程序单元密切相关或易于理解时,可以使用单个头文件来描述整个程序单元组。例如,使用单个头文件来描述 Max 和 Min 函数的行为、Sin、Cos 和 Tan 函数的行为或一组查询封装在包中的对象相关属性的函数是合理的。当集合中的每个函数都能够引发相同的异常时,这尤其重要。

程序单元体头文件

[edit | edit source]

指南

[edit | edit source]
  • 将程序单元维护人员所需的信息放在头文件的主体中。
  • 解释单元如何以及为何执行其功能,而不是解释单元做什么。
  • 不要在头文件中重复代码中显而易见的信息(单元名称除外)。
  • 不要在主体头文件中重复规范头文件中可用的信息(单元名称除外)。

实例化

[edit | edit source]
  • 在头中添加程序单元的名称。
  • 在头文件中记录可移植性问题。
  • 在头文件中总结复杂算法。
  • 记录重大或有争议的实现决策的原因。
  • 记录已舍弃的实现方案,以及舍弃的原因。
  • 在头文件中记录预期的更改,尤其是一些更改已经完成,并且已经对代码进行了更改以使更改易于完成。

示例

[edit | edit source]
------------------------------------------------------------------------
-- Autolayout
--
-- Implementation Notes:
--   - This package uses a heuristic algorithm to minimize the number
--     of arc crossings.  It does not always achieve the true minimum
--     number which could theoretically be reached.  However it does a
--     nearly perfect job in relatively little time.  For details about
--     the algorithm, see ...
--
-- Portability Issues:
--   - The native math package Math_Lib is used for computations of
--     coordinate positions.
--   - 32-bit integers are required.
--   - No operating system specific routines are called.
--
-- Anticipated Changes:
--   - Coordinate_Type below could be changed from integer to float
--     with little effort.  Care has been taken to not depend on the
--     specific characteristics of integer arithmetic.
------------------------------------------------------------------------
package body Autolayout is

   ...

   ---------------------------------------------------------------------
   -- Define
   --
   -- Implementation Notes:
   --   - This routine stores a node in the general purpose Graph data
   --     structure, not the Fast_Graph structure because ...
   ---------------------------------------------------------------------
   procedure Define
         (New_Node : in     Node) is
   begin
      ...
   end Define;

   ---------------------------------------------------------------------
   -- Layout
   --
   -- Implementation Notes:
   --   - This routine copies the Graph data structure (optimized for
   --     fast random access) into the Fast_Graph data structure
   --     (optimized for fast sequential iteration), then performs the
   --     layout, and copies the data back to the Graph structure.  This
   --     technique was introduced as an optimization when the algorithm
   --     was found to be too slow, and it produced an order of
   --     magnitude improvement.
   ---------------------------------------------------------------------
   procedure Layout is
   begin
      ...
   end Layout;

   ---------------------------------------------------------------------
   -- Position_Of
   ---------------------------------------------------------------------
   function Position_Of (Current : in     Node)
         return Position is
   begin
      ...
   end Position_Of;

   ...

end Autolayout;

理由

[edit | edit source]

程序单元主体上的头注释的目的是帮助程序单元的维护人员理解单元的实现,包括不同技术之间的权衡。一定要记录实现过程中做出的所有决策,以防止维护人员犯同样的错误。对维护人员最有价值的评论之一是对为什么正在考虑的更改不起作用的清晰描述。

头文件也是记录可移植性问题的好地方。维护人员可能需要将软件移植到不同的环境中,因此,他们将受益于一个不可移植功能列表。此外,收集和记录可移植性问题会集中关注这些问题,并可能从一开始就产生更可移植的代码。

如果代码难以阅读或理解,则在头文件中总结复杂算法,但不要只是将代码改写一遍。这种重复是不必要的,而且难以维护。类似地,不要重复程序单元规范头文件中的信息。

notes

[edit | edit source]

程序单元通常是自解释的,因此不需要主体头文件来解释如何实现或为什么实现。在这种情况下,完全省略头文件,如上面的 Position_Of 一样。但是,请确保你省略的头文件确实不包含任何信息。例如,请考虑以下两个头文件部分之间的区别

-- Implementation Notes:  None.

-- NonPortable Features:  None.

第一个是作者向维护人员发送的信息,表示“我想不出其他要告诉你的信息”,而第二个可能意味着“我保证这个单元完全可移植”。

数据注释

[edit | edit source]

指南

[edit | edit source]
  • 除非数据类型、对象和异常的名称不言自明,否则请对所有数据类型、对象和异常进行注释。
  • 包括有关复杂、基于指针的数据结构的语义结构的信息。
  • 包括有关数据对象之间维护的关系的信息。
  • 省略只是重复名称中信息的注释。
  • 对于标记类型,在打算让特化(即派生类型)覆盖这些重新分派操作的情况下,包括有关重新分派的信息。

示例

[edit | edit source]

可以按目的对对象进行分组并进行注释,如

...

---------------------------------------------------------------------
-- Current position of the cursor in the currently selected text
-- buffer, and the most recent position explicitly marked by the
-- user.
-- Note:  It is necessary to maintain both current and desired
--        column positions because the cursor cannot always be
--        displayed in the desired position when moving between
--        lines of different lengths.
---------------------------------------------------------------------
Desired_Column : Column_Counter;
Current_Column : Column_Counter;
Current_Row    : Row_Counter;
Marked_Column  : Column_Counter;
Marked_Row     : Row_Counter;

应注释引发异常的条件

---------------------------------------------------------------------
-- Exceptions
---------------------------------------------------------------------
Node_Already_Defined : exception;   -- Raised when an attempt is made
                                    --|   to define a node with an
                                    --|   identifier which already
                                    --|   defines a node.
Node_Not_Defined     : exception;   -- Raised when a reference is
                                    --|   made to a node which has
                                    --|   not been defined.

以下是一个更复杂的示例,它涉及多个记录类型和访问类型,这些类型用于形成复杂的数据结构

---------------------------------------------------------------------
-- These data structures are used to store the graph during the
-- layout process. The overall organization is a sorted list of
-- "ranks," each containing a sorted list of nodes, each containing
-- a list of incoming arcs and a list of outgoing arcs.
-- The lists are doubly linked to support forward and backward
-- passes for sorting. Arc lists do not need to be doubly linked
-- because order of arcs is irrelevant.
--
-- The nodes and arcs are doubly linked to each other to support
-- efficient lookup of all arcs to/from a node, as well as efficient
-- lookup of the source/target node of an arc.
---------------------------------------------------------------------

type Arc;
type Arc_Pointer is access Arc;

type Node;
type Node_Pointer is access Node;

type Node is
   record
      Id       : Node_Pointer;-- Unique node ID supplied by the user.
      Arc_In   : Arc_Pointer;
      Arc_Out  : Arc_Pointer;
      Next     : Node_Pointer;
      Previous : Node_Pointer;
   end record;

type Arc is
   record
      ID     : Arc_ID;        -- Unique arc ID supplied by the user.
      Source : Node_Pointer;
      Target : Node_Pointer;
      Next   : Arc_Pointer;
   end record;

type Rank;
type Rank_Pointer is access Rank;

type Rank is
   record
      Number     : Level_ID;  -- Computed ordinal number of the rank.
      First_Node : Node_Pointer;
      Last_Node  : Node_Pointer;
      Next       : Rank_Pointer;
      Previous   : Rank_Pointer;
   end record;

First_Rank : Rank_Pointer;
Last_Rank  : Rank_Pointer;

理由

[edit | edit source]

添加注释来解释数据结构的用途、结构和语义非常有用。许多维护人员在尝试了解单元的实现时,首先查看数据结构。了解可以存储的数据以及不同数据项之间的关系以及数据在单元中的流动,是了解单元详细信息的重要第一步。

在上面的第一个示例中,Current_Column 和 Current_Row 的名称相对不言自明。Desired_Column 的名称也很好,但它让读者想知道当前列和所需列之间的关系。注释解释了同时拥有两者的原因。

对数据声明进行注释的另一个好处是,声明上的单个注释集可以替换代码中其他地方可能需要的多个注释集。在上面的第一个示例中,注释简要扩展了“当前”和“标记”的含义。它指出“当前”位置是光标的位置,“当前”位置在当前缓冲区中,而“标记”位置是用户标记的位置。此注释以及变量的助记符名称,大大减少了在整个代码中的单个语句中进行注释的需求。

重要的是要记录异常的完整含义以及它们在什么条件下会被引发,如上面的第二个示例所示,尤其是在包规范中声明异常时。读者别无选择,只能通过阅读包主体中的代码来了解异常的确切含义。

将所有异常分组在一起,如第二个示例所示,可以为读者提供“术语表”的效果。当包中的许多不同的子程序可以引发相同的异常时,这很有用。对于每个异常只能被包中的一个子程序引发的包,将相关的子程序和异常分组在一起可能更好。

注释异常时,最好用一般术语描述异常的含义,而不是列出所有可能导致引发异常的子程序;这样的列表更难维护。当添加新例程时,这些列表很可能不会更新。此外,此信息已存在于描述子程序的注释中,这些注释应列出子程序可能引发的所有异常。按子程序列出的异常列表比按异常列出的子程序列表更有用,也更易于维护。

在第三个示例中,记录字段的名称很短,而且是助记符,但它们并不完全不言自明。对于涉及访问类型的复杂数据结构,情况往往如此。没有办法选择记录和字段名称,以便它们完全解释记录和指针到一组嵌套的排序列表中的总体组织。显示的注释在这种情况下很有用。没有它们,读者将不知道哪些列表是排序的,哪些列表是双向链接的,或者为什么。注释表达了作者对这种复杂数据结构的意图。维护人员仍然必须阅读代码,如果他想确保所有双向链接都得到正确的维护。在阅读代码时牢记这一点,可以使维护人员更容易找到一个指针被更新而另一个指针没有被更新的错误。

有关记录重新分派操作使用情况的理由,请参见指南 9.3.1。(重新分派是指将一个基本操作的参数转换为一个类宽类型,并对另一个基本操作进行分派调用。)指南 9.3.1 中的理由讨论了这种文档是否应该放在规范中还是主体中。

语句注释

[edit | edit source]

指南

[edit | edit source]
  • 尽量减少嵌入语句中的注释。
  • 仅使用注释来解释代码中不明显的部分。
  • 对代码中故意省略的部分进行注释。
  • 不要使用注释来解释代码。
  • 不要使用注释来解释远程代码片段,例如当前单元调用的子程序。
  • 在需要注释的地方,使其在视觉上与代码区分开来。

示例

[edit | edit source]

以下是一个注释非常糟糕的代码示例

...

-- Loop through all the strings in the array Strings, converting
-- them to integers by calling Convert_To_Integer on each one,
-- accumulating the sum of all the values in Sum, and counting them
-- in Count.  Then divide Sum by Count to get the average and store
-- it in Average. Also, record the maximum number in the global
-- variable Max_Number.

for I in Strings'Range loop
   -- Convert each string to an integer value by looping through
   -- the characters which are digits, until a nondigit is found,
   -- taking the ordinal value of each, subtracting the ordinal value
   -- of '0', and multiplying by 10 if another digit follows.  Store
   -- the result in Number.
   Number := Convert_To_Integer(Strings(I));
   -- Accumulate the sum of the numbers in Total.
   Sum := Sum + Number;
   -- Count the numbers.
   Count := Count + 1;

   -- Decide whether this number is more than the current maximum.
   if Number > Max_Number then
      -- Update the global variable Max_Number.
      Max_Number := Number;
   end if;

end loop;
-- Compute the average.
Average := Sum / Count;

以下通过不重复代码中显而易见的注释内容,不描述Convert_To_Integer内部的细节,删除错误的注释(累加总和语句上的注释),并使剩余的几个注释在视觉上与代码区分开来,得到了改进。

Sum_Integers_Converted_From_Strings:
   for I in Strings'Range loop
      Number := Convert_To_Integer(Strings(I));
      Sum := Sum + Number;
      Count := Count + 1;

      -- The global Max_Number is computed here for efficiency.
      if Number > Max_Number then
         Max_Number := Number;
      end if;

   end loop Sum_Integers_Converted_From_Strings;

Average := Sum / Count;

理由

[edit | edit source]

示例中显示的改进,不仅仅是通过减少注释总数来实现的;它们是通过减少无用注释的数量来实现的。

解释代码的明显方面的注释毫无价值。它们浪费了作者的写作时间和维护人员的更新时间。因此,它们往往最终会变得不正确。此类注释还会使代码混乱,隐藏了少数重要的注释。

描述另一个单元内部发生的事情的注释违反了信息隐藏原则。关于Convert_To_Integer的细节(上面已删除)与调用单元无关,最好隐藏起来,以防算法发生变化。解释代码中其他地方发生的事情的示例非常难以维护,并且几乎总是在第一次代码修改时就变得不正确。

使注释在视觉上与代码区分开来的好处在于,它使代码更容易扫描,并且少数重要的注释更容易突出。突出显示不寻常或特殊的代码特征表明它们是故意的。这通过将注意力集中在维护或将程序移植到另一个实现时可能导致问题的代码部分,帮助维护人员。

注释应用于记录不可移植、实现相关、环境相关或以任何方式棘手的代码。它们通知读者,一些不寻常的内容在那里是有原因的。一个有益的注释是解释编译器错误解决方法的注释。如果您使用的是更低级别的(在软件工程意义上不是“理想的”)解决方案,请对其进行注释。注释中包含的信息应说明您为什么使用该特定结构。还应包含有关失败尝试的文档,例如,使用更高级别的结构。这种注释对于维护人员来说,具有历史意义。您向读者展示了在选择结构时,已经进行了一系列认真的思考。

最后,注释应用于解释代码中不存在的内容,以及存在的内容。如果您做出了不执行某些操作的决定,例如释放您似乎已经完成的数据结构,请务必添加注释解释为什么不这样做。否则,维护人员可能会注意到明显的遗漏,并在以后对其进行“更正”,从而引入错误。

另请参见准则 9.3.1,了解有关您应提供的有关标记类型和重新分派的文档类型的讨论。

notes

[edit | edit source]

可以通过在局部块中声明变量CountSum,使它们的范围受限,并且它们的初始化在使用附近发生,从而对上述示例进行进一步改进,例如,通过命名块Compute_Average或通过将代码移至名为Average_Of的函数中。Max_Number的计算也可以与Average的计算分离。但是,这些更改是其他准则的主题;本示例仅用于说明注释的正确用法。

标记注释

[edit | edit source]

指南

[edit | edit source]
  • 使用分页标记来标记程序单元边界(请参见准则 2.1.7)。
  • 如果begin之前有声明,则在注释中重复单元名称以标记包体、子程序体、任务体或块的begin
  • 对于长或嵌套很深的ifcase语句,使用注释标记语句的结尾,以总结控制语句的条件。
  • 对于长或嵌套很深的if语句,使用注释标记else部分,以总结控制此部分语句的条件。

示例

[edit | edit source]
if    A_Found then
   ...
elsif B_Found then
   ...

else  -- A and B were both not found
   ...

   if Count = Max then
      ...

   end if;

   ...
end if;  -- A_Found

------------------------------------------------------------------------
package body Abstract_Strings is
   ...

   ---------------------------------------------------------------------
   procedure Concatenate (...) is
   begin
      ...
   end Concatenate;
   ---------------------------------------------------------------------

   ...
begin  -- Abstract_Strings
   ...
end Abstract_Strings;
------------------------------------------------------------------------

理由

[edit | edit source]

标记注释强调代码的结构,并使其更容易扫描。它们可以是用于分离代码部分的行,或用于构造的描述性标签。它们帮助读者解决关于代码中当前位置的问题。对于大型单元来说,这比小型单元更重要。一个简短的标记注释可以与它关联的保留字放在同一行。因此,它可以添加信息而不会造成混乱。

if语句的ifelsifelseend if通常由长语句序列隔开,有时会涉及其他if语句。如第一个示例所示,标记注释在很长的视觉距离内强调了同一语句的关键字之间的关联。对于块语句和循环语句,标记注释不是必需的,因为这些语句的语法允许它们使用在结尾重复的名称进行命名。使用这些名称比使用标记注释更好,因为编译器会验证开头和结尾处的名称是否匹配。

包体的语句序列通常与包的第一行相距很远。许多子程序体,每个子程序体都包含许多begin行,可能首先出现。如第二个示例所示,标记注释强调了begin与包的关联。

notes

[edit | edit source]

如果过度使用,重复名称和注释条件表达式会使代码混乱。正是视觉距离,尤其是分页符,使标记注释变得有益。

使用类型

[edit | edit source]

强类型化在软件中促进了可靠性。对象的类型定义定义了所有合法的值和操作,并允许编译器在编译期间检查和识别潜在错误。此外,类型规则允许编译器生成代码来检查执行期间对类型约束的违反。与类型较弱的语言相比,使用这些 Ada 编译器的功能可以实现更早、更完整的错误检测。

声明类型

[edit | edit source]

指南

[edit | edit source]
  • 尽可能限制标量类型的范围。
  • 从应用程序中查找有关可能值的信息。
  • 不要重用包Standard中的任何子类型名称。
  • 使用子类型声明来提高程序可读性(Booch 1987)。
  • 将派生类型和子类型结合使用(请参见准则 5.3.1)。

示例

[edit | edit source]
subtype Card_Image is String (1 .. 80);
Input_Line : Card_Image := (others => ' ');
-- restricted integer type:
type    Day_Of_Leap_Year     is                  range 1 .. 366;
subtype Day_Of_Non_Leap_Year is Day_Of_Leap_Year range 1 .. 365;

通过以下声明,程序员的意思是,“我完全不知道有多少”,但实际的基本范围将出现在代码中或作为系统参数被隐藏。

Employee_Count : Integer;

理由

[edit | edit source]

从合法范围内消除无意义的值可以提高编译器在对象设置为无效值时检测错误的能力。这也提高了程序的可读性。此外,它迫使您仔细考虑对声明为子类型对象的每个使用。

不同的实现为大多数预定义类型提供了不同的值集。读者无法从预定义名称中确定预期的范围。当预定义名称被重载时,这种情况会更加严重。

对象及其子类型的名称可以阐明其预期的用途,并记录低级设计决策。上面的示例记录了一个设计决策,即将软件限制为物理参数源自穿孔卡特性的设备。此信息很容易找到,以便进行任何后续更改,从而增强了程序的可维护性。

可以通过声明没有约束的子类型来重命名类型(Ada 参考手册 1995,第 8.5 节 [注释])。不能重载子类型名称;重载仅适用于可调用实体。枚举文字被视为无参数函数,因此包含在此规则中。

类型可以具有高度受限的值集,而不会消除有用的值。如准则 5.3.1 中所述的用法消除了可执行语句中的许多标志变量和类型转换。这使得程序更易读,同时允许编译器强制执行强类型约束。

notes

[edit | edit source]

子类型声明不会定义新的类型,而只是为现有类型定义约束。

任何偏离此准则的行为都会损害 Ada 语言强类型设施的优势。

异常

[edit | edit source]

在某些情况下,您不依赖任何数值范围。例如,在数组索引中会出现这种情况(例如,大小不受任何特定语义限制的列表)。有关预定义类型的适当用法的讨论,请参见准则 7.2.1。

枚举类型

[edit | edit source]

指南

[edit | edit source]
  • 使用枚举类型而不是数字代码。
  • 只有在绝对必要时,才使用表示子句来匹配外部设备的要求。

示例

[edit | edit source]

使用

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

而不是

Blue   : constant := 1;
Red    : constant := 2;
Green  : constant := 3;
Yellow : constant := 4;

并在必要时添加以下内容

for Color use (Blue   => 1,
               Red    => 2,
               Green  => 3,
               Yellow => 4);

理由

[edit | edit source]

枚举类型比数字代码更健壮;它们减少了因解释错误以及维护期间的值集添加和删除而导致的错误的可能性。数字代码是来自没有用户定义类型的语言的遗留物。

此外,Ada 为枚举类型提供了许多属性('Pos、'Val、'Succ、'Pred、'Image 和 'Value),这些属性在使用时比用户编写的编码操作更可靠。

数字代码可能乍一看似乎适合匹配外部值。相反,这些情况需要对枚举类型使用表示子句。表示子句记录了“编码”。如果程序结构合理,以便隔离和封装硬件依赖项(参见准则 7.1.5),数字代码最终会出现在接口包中,在那里可以轻松找到和替换,以防需求发生变化。

一般来说,避免对枚举类型使用表示子句。如果没有枚举文字的明显排序,如果必须重新排序枚举类型以适应新平台上的表示顺序变化,则枚举表示可能会导致可移植性问题。

总结

[edit | edit source]

拼写

[edit | edit source]
  • 使用下划线分隔复合名称中的单词。
  • 以一致的方式表示数字。
  • 以适合问题的基数表示字面量。
  • 使用下划线分隔数字,就像在普通文本中使用逗号或句号(或非十进制基数的空格)一样。
  • 使用科学计数法时,始终使 E 为大写或小写。
  • 在备用基数中,以全大写或全小写表示字母字符。
  • 使保留字和其他程序元素在视觉上彼此区分。
  • 不要在存在更短同义词的情况下,使用长单词的缩写作为标识符。
  • 使用一致的缩写策略。
  • 不要使用模棱两可的缩写。
  • 为了证明缩写的必要性,缩写必须比完整单词节省许多字符。
  • 使用在应用领域中被广泛接受的缩写。
  • 维护一个已接受缩写的列表,并且只使用该列表中的缩写。

命名约定

[edit | edit source]
  • 选择尽可能自说明的名称。
  • 使用简短的同义词而不是缩写。
  • 使用应用程序提供的名称,但不要使用晦涩的术语。
  • 避免使用相同名称来声明不同类型的标识符。
  • 使用单数、通用名词作为子类型标识符。
  • 选择描述子类型之一的值的标识符。
  • 考虑为定义可见访问类型、可见子范围或可见数组类型的子类型标识符使用后缀。
  • 对于私有类型,不要使用仅限于子类型标识符的标识符结构(例如,后缀)。
  • 不要使用预定义包中的子类型名称。
  • 对布尔对象使用谓词子句或形容词。
  • 使用单数、具体名词作为对象标识符。
  • 选择描述对象在执行期间的值的标识符。
  • 使用单数、通用名词作为记录组件的标识符。
  • 对标记类型和关联包使用一致的命名约定。
  • 对过程和条目使用动作动词。
  • 对布尔函数使用谓词子句。
  • 对非布尔函数使用名词。
  • 为包提供暗示比子程序更高级别组织的名称。通常,这些是描述所提供抽象的名词短语。
  • 为任务提供暗示活动实体的名称。
  • 对受保护单元使用描述正在保护的数据的名词。
  • 考虑将泛型子程序命名为非泛型子程序。
  • 考虑将泛型包命名为非泛型包。
  • 使泛型名称比实例化名称更通用。
  • 尽可能使用符号值而不是文字。
  • 对数学常量 Pi 和 e 使用预定义常量 Ada.Numerics.Pi 和 Ada.Numerics.e。
  • 对常数值使用常量而不是变量。
  • 当值特定于类型或当值必须是静态时使用常量。
  • 尽可能使用命名数字而不是常量。
  • 使用命名数字替换类型或上下文真正通用的数字文字。
  • 对于在详细说明后其值无法更改的对象,使用常量。(联合技术公司 1987 年)。
  • 通过使用静态表达式定义它们来显示符号值之间的关系。
  • 使用线性无关的文字集。
  • 尽可能使用 'First 和 'Last 等属性而不是文字。
  • 使用一个名称来指示异常代表的问题类型。
  • 在命名构造函数(从这个意义上讲,用于创建和/或初始化对象的运算)时,包含 New、Make 或 Create 之类的前缀。
  • 对于包含构造函数的子包,请使用反映其内容的名称。

注释

[edit | edit source]
  • 使代码尽可能清晰,以减少对注释的需求。
  • 切勿在注释中重复代码中已有的信息。
  • 在需要注释的地方,注释应该简洁完整。
  • 在注释中使用正确的语法和拼写。
  • 使注释在视觉上与代码区分开来。
  • 在标题中构建注释,以便工具可以自动提取信息。
  • 每个源文件中都需要一个文件头。
  • 将文件的所有权、责任和历史信息放在文件头中。
  • 在每个程序单元的规范中添加一个头。
  • 将程序单元用户所需的信息放在规范头中。
  • 不要重复规范头中已有的信息(单元名称除外)。
  • 解释单元的功能,而不是解释它如何或为何执行该功能。
  • 描述程序单元的完整接口,包括它可能引发的任何异常以及它可能产生的任何全局影响。
  • 不要包含有关单元如何适应封闭软件系统的信息。
  • 描述单元的性能(时间和空间)特征。
  • 将程序单元维护人员所需的信息放置在主体标题中。
  • 解释单元如何以及为何执行其功能,而不是解释单元做什么。
  • 不要在头文件中重复代码中显而易见的信息(单元名称除外)。
  • 不要在主体头文件中重复规范头文件中可用的信息(单元名称除外)。
  • 除非数据类型、对象和异常的名称不言自明,否则请对所有数据类型、对象和异常进行注释。
  • 包括有关复杂、基于指针的数据结构的语义结构的信息。
  • 包括有关数据对象之间维护的关系的信息。
  • 省略只是重复名称中信息的注释。
  • 对于标记类型,在打算让特化(即派生类型)覆盖这些重新分派操作的情况下,包括有关重新分派的信息。
  • 尽量减少嵌入语句中的注释。
  • 仅使用注释来解释代码中不明显的部分。
  • 对代码中故意省略的部分进行注释。
  • 不要使用注释来解释代码。
  • 不要使用注释来解释远程代码片段,例如当前单元调用的子程序。
  • 在需要注释的地方,使其在视觉上与代码区分开来。
  • 使用分页标记来标记程序单元边界。
  • 如果 begin 之前有声明,则在注释中重复单元名称以标记包体、子程序体、任务体或块的开头。
  • 对于长或严重嵌套的 if 和 case 语句,使用注释标记语句的结尾,总结控制语句的条件。
  • 对于长或严重嵌套的 if 语句,使用注释标记 else 部分,总结控制此部分语句的条件。

使用类型

[edit | edit source]
  • 尽可能限制标量类型的范围。
  • 从应用程序中查找有关可能值的信息。
  • 不要重用包Standard中的任何子类型名称。
  • 使用子类型声明来提高程序可读性(Booch 1987)。
  • 协同使用派生类型和子类型。
  • 使用枚举类型而不是数字代码。
  • 只有在绝对必要时,才使用表示子句来匹配外部设备的要求。

程序结构

华夏公益教科书