跳转到内容

Ada 编程/异常

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

Ada. Time-tested, safe and secure.
Ada. 经久耐用、安全可靠。

健壮性

[编辑 | 编辑源代码]

健壮性是指系统或系统组件在检测到异常时能够“合理”地运行的能力,例如:

  • 它接收无效的输入。
  • 另一个系统组件(硬件或软件)出现故障。

以电话交换机控制程序为例。当线路故障时,控制程序应该怎么做?简单地停止是不可接受的——所有通话都将失败。更好的方法是放弃当前通话(仅此而已),记录线路已停用,然后继续。更好的方法是尝试重用线路——故障可能是瞬时的。健壮性在所有系统中都是可取的,但在依赖于人类安全或福祉的系统中至关重要,例如医院病人监测、飞机电传操纵、核电站控制等。

模块、先决条件和后置条件

[编辑 | 编辑源代码]

模块可以使用其先决条件和后置条件来指定。先决条件是模块的输入应该满足的条件。后置条件是模块的输出需要满足的条件,前提是先决条件得到满足。如果模块的先决条件没有得到满足,它应该怎么做?

  • 停止?即使有诊断信息,这通常也是不可接受的。
  • 使用全局结果代码?结果代码可以设置为指示异常。随后,它可以由可以执行错误恢复的模块进行测试。问题:这会引起所涉及模块之间的紧密耦合。
  • 每个模块都有自己的结果代码?这是一个参数(或函数结果),可以设置为指示异常,并由调用模块进行测试。问题:(1) 设置和测试结果代码往往会淹没正常情况逻辑,(2) 结果代码通常被忽略。
  • 异常处理——Ada 的解决方案。检测到异常的模块会引发异常。相同或另一个模块可以处理该异常。

异常机制允许干净、模块化地处理异常情况

  • 一个单元(例如,块或子程序体)可以引发异常,以表明已检测到异常。引发异常的计算将被放弃(并且永远无法恢复,尽管它可以重新启动)。
  • 一个单元可以传播由自身引发的异常(或从它调用的另一个单元传播出来的异常)。
  • 或者,一个单元可以处理此类异常,允许程序员定义从异常情况中恢复。异常处理程序与正常情况代码分离。

预定义异常

[编辑 | 编辑源代码]

预定义异常是在包 Standard 中定义的。每个语言定义的运行时错误都会导致引发预定义异常。以下是一些示例:

  • Constraint_Error,当子类型的约束没有得到满足时引发
  • Program_Error,当在受保护对象内部调用受保护操作时引发,例如:
  • Storage_Error,当存储空间不足时引发
  • Tasking_Error,当任务无法激活,因为操作系统没有足够的资源时引发,例如:

例 1

  Name : String (1 .. 10);
  ...
  Name := "Hamlet"; -- Raises Constraint_Error,
                    -- because the "Hamlet" has bounds (1 .. 6).

例 2

  loop
     P := new Int_Node'(0, P);
  end loop; -- Soon raises Storage_Error,
            -- because of the extreme memory leak.

例 3 比较以下方法

  procedure Compute_Sqrt (X    : in  Float;
                          Sqrt : out Float;
                          OK   : out Boolean)
  is
  begin
     if X >= 0 then
        OK := True;
        -- compute √X
        ...
     else
        OK := False;
     end if;
  end Compute_Sqrt;
  
  ...
  
  procedure Triangle (A, B, C         : in  Float;
                      Area, Perimeter : out Float;
                      Exists          : out Boolean)
  is
     S  : constant Float := 0.5 * (A + B + C);
     OK : Boolean;
  begin
     Compute_Sqrt (S * (S-A) * (S-B) * (S-C), Area, OK);
     Perimeter := 2.0 * S;
     Exists    := OK;
  end Triangle;

对 Compute_Sqrt 的负参数会导致 OK 设置为 False。Triangle 使用它来确定其自身状态参数值,依此类推,一直到调用树的顶端,ad nauseam

对比

  function Sqrt (X : Float) return Float is
  begin
     if X < 0.0 then
        raise Constraint_Error;
     end if;
     -- compute √X
     ...
  end Sqrt;
  
  ...
  
  procedure Triangle (A, B, C         : in  Float;
                      Area, Perimeter : out Float)
  is
     S: constant Float := 0.5 * (A + B + C);
  begin
     Area      := Sqrt (S * (S-A) * (S-B) * (S-C));
     Perimeter := 2.0 * S;
  end Triangle;

对 Sqrt 的负参数会导致 Constraint_Error 在 Sqrt 内部显式引发,并传播出去。Triangle 只是传播异常(不处理它)。

或者,我们可以使用类型系统来捕获错误

  subtype Pos_Float is Float range 0.0 .. Float'Last;
  
  function Sqrt (X : Pos_Float) return Pos_Float is
  begin
     -- compute √X
     ...
  end Sqrt;

对 Sqrt 的负参数现在会在调用点引发 Constraint_Error。Sqrt 甚至从未进入。

输入输出异常

[编辑 | 编辑源代码]

预定义包 Ada.Text_IO 的子程序引发的一些异常示例如下:

  • End_Error,当 Get、Skip_Line 等在已达到文件末尾时引发。
  • Data_Error,当 Get 在 Integer_IO 等中引发,如果输入不是预期的类型的字面量。
  • Mode_Error,当尝试从输出文件读取或向输入文件写入时引发,等等。
  • Layout_Error,当在文本 I/O 操作中指定无效数据格式时引发

例 1

  declare
     A : Matrix (1 .. M, 1 .. N);
  begin
     for I in 1 .. M loop
        for J in 1 .. N loop
            begin
               Get (A(I,J));
            exception
               when Data_Error =>
                  Put ("Ill-formed matrix element");
                  A(I,J) := 0.0;
            end;
         end loop;
     end loop;
  exception
     when End_Error =>
        Put ("Matrix element(s) missing");
  end;

异常声明

[编辑 | 编辑源代码]

异常的声明方式与对象类似。

例 1 声明了两个异常

  Line_Failed, Line_Closed: exception;

但是,异常不是对象。例如,对声明了异常的范围的递归重新进入不会创建相同名称的新异常;相反,会重复使用外部调用中声明的异常。

例 2

  package Directory_Enquiries is

     procedure Insert (New_Name   : in Name;
                       New_Number : in Number);

     procedure Lookup (Given_Name  : in  Name;
                       Corr_Number : out Number);

     Name_Duplicated : exception;
     Name_Absent     : exception;
     Directory_Full  : exception;

  end Directory_Enquiries;

异常处理程序

[编辑 | 编辑源代码]

当发生异常时,正常的执行流程将被放弃,异常将被传递到调用序列中,直到找到匹配的处理程序。任何声明区域(除了包规范)都可以有处理程序。处理程序指定它将处理的异常。通过向上移动调用序列,异常可以变为匿名;在这种情况下,它们只能由以下处理程序进行处理:others 处理程序。

function F return Some_Type is
  ... -- declarations (1)
begin
  ... -- statements (2)
exception -- handlers start here (3)
  when Name_1 | Name_2 => ... -- The named exceptions are handled with these statements
  when others => ...  -- any other exceptions (also anonymous ones) are handled here
end F;

在声明区域本身 (1) 引发的异常 (1) 不能由该区域的处理程序 (3) 处理;它们只能在外部作用域中进行处理。在语句序列 (2) 中引发的异常当然可以在 (3) 中进行处理。

这条规则的原因是,处理程序可以假设在声明区域 (1) 中声明的任何项目都是定义良好的,并且可以被引用。如果 (3) 处的处理程序可以处理在 (1) 处引发的异常,则将不知道哪些项目存在,哪些项目不存在。

引发异常

[编辑 | 编辑源代码]

raise 语句显式引发指定异常。

例 1

  package body Directory_Enquiries is
  
     procedure Insert (New_Name   : in Name;
                       New_Number : in Number)
     isbeginif New_Name = Old_Entry.A_Name then
           raise Name_Duplicated;
        end if;
        …
        New_Entry :=  new Dir_Node'(New_Name, New_Number,…);
        …
     exception
        when Storage_Error => raise Directory_Full;
     end Insert;
     
     procedure Lookup (Given_Name  : in  Name;
                       Corr_Number : out Number)
     isbeginif not Found then
           raise Name_Absent;
        end if;
        …
     end Lookup;
  
  end Directory_Enquiries;

异常处理和传播

[编辑 | 编辑源代码]

异常处理程序可以分组在块、子程序体等的末尾。处理程序是任何可能以以下方式结束的语句序列:

  • 通过完成;
  • 通过执行return 语句;
  • 通过引发另一个异常 (raise e;);
  • 通过重新引发相同异常 (raise;)。

假设在语句序列U(块、子程序体等)中引发了异常e

  • 如果U 包含e 的处理程序:将执行该处理程序,然后控制权将离开U
  • 如果U 不包含e 的处理程序:e 将被传播U 的外部;实际上,eU 的“调用点”处引发。

因此,引发异常会导致导致异常发生的语句序列在异常发生点被放弃。它不会也不可能恢复。

例 1

  ...
  exception
     when Line_Failed =>
        begin -- attempt recovery
           Log_Error;
           Retransmit (Current_Packet);
        exception
           when Line_Failed =>
              Notify_Engineer; -- recovery failed!
              Abandon_Call;
        end;
  ...

有关异常发生的详细信息

[编辑 | 编辑源代码]

Ada 在 Ada.Exceptions 中提供有关异常的信息,该信息位于类型 Exception_Occurrence 的对象中,以及以该类型作为参数的子程序

  • Exception_Name: 使用点表示法并以大写字母返回完整的异常名称。例如:Queue.Overflow.
  • Exception_Message: 返回与事件相关的异常消息。
  • Exception_Information: 返回包含异常名称和相关异常消息的字符串。

要获取异常事件对象,可以使用以下语法

with Ada.Exceptions;  use Ada.Exceptions;
...
exception
  when Error: High_Pressure | High_Temperature =>
    Put ("Exception: ");
    Put_Line (Exception_Name (Error));
    Put (Exception_Message (Error));
  when Error: others =>
    Put ("Unexpected exception: ");
    Put_Line (Exception_Information(Error));
end;

当异常消息内容未由引发异常的用户设置时,它是实现定义的。它通常包含异常的原因和引发位置。

用户可以使用 Raise_Exception 过程指定消息。

declare
   Valve_Failure : exception;
begin
  ...
  Raise_Exception (Valve_Failure'Identity, "Failure while opening");
  ...
  Raise_Exception (Valve_Failure'Identity, "Failure while closing");
  ...
exception
  when Fail: Valve_Failure =>
    Put (Exception_Message (Fail));
end;


从 Ada 2005 开始,可以使用更简单的语法将字符串消息与异常事件关联。

-- This language feature is only available from Ada 2005 on.
declare
   Valve_Failure : exception;
begin
  ...
  raise Valve_Failure with "Failure while opening";
  ...
  raise Valve_Failure with "Failure while closing";
  ...
exception
  when Fail: Valve_Failure =>
    Put (Exception_Message (Fail));
end;

Ada.Exceptions 包还提供用于保存异常事件和重新引发它们的子程序。

另请参阅

[编辑 | 编辑源代码]

维基教科书

[编辑 | 编辑源代码]

Ada 95 参考手册

[编辑 | 编辑源代码]

Ada 2005 参考手册

[编辑 | 编辑源代码]

Ada 质量和风格指南

[编辑 | 编辑源代码]
华夏公益教科书