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 >= 0then
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
Floatis
begin
if
X < 0.0then
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_Floatis
Floatrange
0.0 .. Float'Last;function
Sqrt (X : Pos_Float)return
Pos_Floatis
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
Iin
1 .. Mloop
for
Jin
1 .. Nloop
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_Enquiriesis
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
Freturn
Some_Typeis
... -- declarations (1)begin
... -- statements (2)exception
-- handlers start here (3)when
Name_1 | Name_2 => ... -- The named exceptions are handled with these statementswhen
others
=> ... -- any other exceptions (also anonymous ones) are handled hereend
F;
在声明区域本身 (1) 引发的异常 (1) 不能由该区域的处理程序 (3) 处理;它们只能在外部作用域中进行处理。在语句序列 (2) 中引发的异常当然可以在 (3) 中进行处理。
这条规则的原因是,处理程序可以假设在声明区域 (1) 中声明的任何项目都是定义良好的,并且可以被引用。如果 (3) 处的处理程序可以处理在 (1) 处引发的异常,则将不知道哪些项目存在,哪些项目不存在。
raise 语句显式引发指定异常。
例 1
package
body
Directory_Enquiriesis
procedure
Insert (New_Name :in
Name; New_Number :in
Number)is
…begin
…if
New_Name = Old_Entry.A_Namethen
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)is
…begin
…if
not
Foundthen
raise
Name_Absent;end
if
; …end
Lookup;end
Directory_Enquiries;
异常处理程序可以分组在块、子程序体等的末尾。处理程序是任何可能以以下方式结束的语句序列:
- 通过完成;
- 通过执行return 语句;
- 通过引发另一个异常 (raise e;);
- 通过重新引发相同异常 (raise;)。
假设在语句序列U(块、子程序体等)中引发了异常e。
- 如果U 包含e 的处理程序:将执行该处理程序,然后控制权将离开U。
- 如果U 不包含e 的处理程序:e 将被传播到U 的外部;实际上,e 在U 的“调用点”处引发。
因此,引发异常会导致导致异常发生的语句序列在异常发生点被放弃。它不会也不可能恢复。
例 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_Failurewith
"Failure while opening"; ...raise
Valve_Failurewith
"Failure while closing"; ...exception
when
Fail: Valve_Failure => Put (Exception_Message (Fail));end
;
Ada.Exceptions 包还提供用于保存异常事件和重新引发它们的子程序。
- 第 4 章:程序结构
- 第 5 章:编程实践
- 第 7 章:可移植性