第17课|异常处理

10 阅读7分钟

本课目标

完成本课后,你将能够:

  • 理解异常的概念与 Ada 预定义异常
  • 掌握异常处理器的编写与异常传播
  • 学习自定义异常的声明与触发
  • 运用异常编写健壮、容错的代码

一、异常的基本概念

1.1 什么是异常

异常是程序运行时发生的错误或特殊情况,如除零、数组越界、内存不足等。Ada 提供异常处理机制,允许程序捕获异常并采取适当措施,避免崩溃。

procedure Divider is
   A, B : Integer;
begin
   A := 10;
   B := 0;
   A := A / B;   -- 这里会触发 Constraint_Error
exception
   when Constraint_Error =>
      Ada.Text_IO.Put_Line ("除以零错误!");
end Divider;

1.2 异常的优势

  • 将错误处理代码与正常逻辑分离
  • 可以在调用栈中向上传播,由适当的层级处理
  • 强制处理或至少意识到潜在错误

二、预定义异常

Ada 定义了一些标准异常,在特定错误发生时自动触发。

异常触发场景
Constraint_Error范围检查失败、数组索引越界、除零、空指针访问等
Program_Error程序逻辑错误(如未初始化的子程序调用)
Storage_Error内存不足或栈溢出
Tasking_Error任务相关的错误(并发编程)
Numeric_Error数值运算错误(已被 Constraint_Error 取代,保留用于兼容)

2.1 Constraint_Error 示例

procedure Demo_Constraint is
   subtype Small is Integer range 1 .. 10;
   X : Small := 5;
begin
   X := X + 10;   -- 超出范围,触发 Constraint_Error
exception
   when Constraint_Error =>
      Ada.Text_IO.Put_Line ("范围溢出");
end Demo_Constraint;

2.2 Program_Error 示例

procedure Demo_Program_Error is
   type Func_Ptr is access function (X : Integer) return Integer;
   P : Func_Ptr;   -- 未初始化
begin
   -- P.all (5);    -- 调用空访问值,触发 Program_Error
   null;
exception
   when Program_Error =>
      Ada.Text_IO.Put_Line ("程序逻辑错误");
end Demo_Program_Error;

2.3 Storage_Error 示例

procedure Demo_Storage is
   type Big_Array is array (1 .. Integer'Last) of Integer;
   A : Big_Array;   -- 可能触发 Storage_Error
begin
   null;
exception
   when Storage_Error =>
      Ada.Text_IO.Put_Line ("内存不足");
end Demo_Storage;

三、异常处理器的编写

3.1 基本语法

exception 部分放在子程序或包的 end 之前,包含一个或多个 when 分支:

procedure Example is
begin
   -- 可能触发异常的代码
   ...
exception
   when Constraint_Error =>
      -- 处理约束错误
      Ada.Text_IO.Put_Line ("约束错误");
   when Program_Error | Storage_Error =>
      -- 处理多个异常
      Ada.Text_IO.Put_Line ("系统错误");
   when others =>
      -- 捕获所有未列出的异常
      Ada.Text_IO.Put_Line ("未知错误");
end Example;

3.2 when others 分支

when others 必须放在最后,作为默认处理器。推荐至少记录错误信息,避免异常被静默忽略。

exception
   when Constraint_Error =>
      ...
   when others =>
      Ada.Text_IO.Put_Line ("未预期的异常");
      raise;   -- 重新抛出当前异常,让上层处理

3.3 异常处理器中的 raise

可以在处理器中重新抛出异常,让上层继续处理:

procedure Sub_Proc is
begin
   ...
exception
   when Constraint_Error =>
      Ada.Text_IO.Put_Line ("Sub_Proc 中捕获,重新抛出");
      raise;   -- 重新抛出同一异常
end Sub_Proc;

四、自定义异常

4.1 声明异常

可以在任何声明区域定义自己的异常:

package Stack is
   Stack_Empty : exception;
   Stack_Full  : exception;
   procedure Push (Item : Integer);
   procedure Pop (Item : out Integer);
end Stack;

4.2 触发异常

使用 raise 语句触发异常,可以附带错误消息(Ada 2005+):

package body Stack is
   Max_Size : constant := 100;
   type Stack_Array is array (1 .. Max_Size) of Integer;
   Data : Stack_Array;
   Top  : Integer := 0;
   
   procedure Push (Item : Integer) is
   begin
      if Top = Max_Size then
         raise Stack_Full;
      end if;
      Top := Top + 1;
      Data (Top) := Item;
   end Push;
   
   procedure Pop (Item : out Integer) is
   begin
      if Top = 0 then
         raise Stack_Empty;
      end if;
      Item := Data (Top);
      Top := Top - 1;
   end Pop;
end Stack;

4.3 带消息的 raise

raise Stack_Full with "Stack capacity exceeded";

捕获时可通过 Exception_Message 获取消息:

with Ada.Exceptions; use Ada.Exceptions;

exception
   when E : Stack_Full =>
      Ada.Text_IO.Put_Line (Exception_Message (E));

五、异常的传播

5.1 传播机制

如果异常在当前子程序中没有被处理,它会向调用者传播,直到被某个处理器捕获。如果传播到主程序仍未处理,程序终止。

procedure Level3 is
begin
   raise Constraint_Error;
end Level3;

procedure Level2 is
begin
   Level3;
exception
   when Program_Error =>
      Ada.Text_IO.Put_Line ("Level2 捕获 Program_Error");
end Level2;

procedure Level1 is
begin
   Level2;
exception
   when Constraint_Error =>
      Ada.Text_IO.Put_Line ("Level1 捕获 Constraint_Error");
end Level1;

调用 Level1 输出:

Level1 捕获 Constraint_Error

因为 Level3 抛出的 Constraint_Error 未被 Level2 捕获(Level2 只处理 Program_Error),所以传播到 Level1

5.2 未处理异常

未捕获的异常会导致程序终止,并输出错误信息(取决于运行时环境)。

procedure Unhandled is
begin
   raise Constraint_Error;
end Unhandled;
-- 运行时输出类似:
-- raised CONSTRAINT_ERROR : unhandled exception

六、异常与子程序参数

6.1 子程序内部异常

子程序内部发生的异常,如果未被处理,会传播到调用者。参数的值可能不确定(对于 outin out 参数,如果异常发生在赋值之后,其值可能已改变)。Ada 不保证参数的原子性。

procedure Set_Value (X : out Integer) is
begin
   X := 10;
   raise Constraint_Error;   -- 传播前 X 已被赋值
end Set_Value;

6.2 避免部分更新

如果需要在异常时回滚状态,可以先用局部变量计算,确认无异常后再赋值:

procedure Safe_Update (Target : out Integer; Source : Integer) is
   Temp : Integer;
begin
   Temp := Source * 2;
   if Temp > 100 then
      raise Constraint_Error;
   end if;
   Target := Temp;   -- 只有成功时才修改输出参数
end Safe_Update;

七、异常与包初始化

包体初始化部分(begin ... end)抛出的异常会导致程序终止,除非在初始化代码中捕获。

package Init_Error is
   procedure P;
end Init_Error;

package body Init_Error is
   procedure P is
   begin
      null;
   end P;
begin
   Ada.Text_IO.Put_Line ("Initializing...");
   raise Constraint_Error;   -- 程序会终止
end Init_Error;

要优雅处理,可以在初始化代码内部使用异常处理器:

begin
   begin
      -- 可能失败的初始化
      ...
   exception
      when Constraint_Error =>
         Ada.Text_IO.Put_Line ("Init failed, using defaults");
   end;
end Init_Error;

八、完整示例:文件读取与异常处理

-- 文件名: file_reader.adb
-- 功能:读取文件并处理各种异常

with Ada.Text_IO;
with Ada.Exceptions; use Ada.Exceptions;

procedure File_Reader is
   
   File_Name : constant String := "data.txt";
   File      : Ada.Text_IO.File_Type;
   Line      : String (1 .. 100);
   Last      : Integer;
   
   -- 自定义异常
   File_Empty : exception;
   
   -- 读取并处理文件
   procedure Process_File (Name : String) is
      Line_Count : Integer := 0;
   begin
      Ada.Text_IO.Open (File, Ada.Text_IO.In_File, Name);
      Ada.Text_IO.Put_Line ("Opened file: " & Name);
      
      loop
         Ada.Text_IO.Get_Line (File, Line, Last);
         Line_Count := Line_Count + 1;
         Ada.Text_IO.Put_Line (Integer'Image (Line_Count) & ": " & Line (1 .. Last));
      end loop;
      
   exception
      when Ada.Text_IO.End_Error =>
         Ada.Text_IO.Put_Line ("End of file reached. Total lines: " & 
                               Integer'Image (Line_Count));
         if Line_Count = 0 then
            raise File_Empty;
         end if;
         Ada.Text_IO.Close (File);
         
      when Ada.Text_IO.Name_Error =>
         Ada.Text_IO.Put_Line ("File not found: " & Name);
         raise;
         
      when Ada.Text_IO.Status_Error =>
         Ada.Text_IO.Put_Line ("File already open or invalid");
         
      when others =>
         Ada.Text_IO.Put_Line ("Unexpected error: " & Exception_Information (Exception_Information'First));
   end Process_File;
   
begin
   Process_File (File_Name);
   
exception
   when File_Empty =>
      Ada.Text_IO.Put_Line ("File is empty");
      
   when E : others =>
      Ada.Text_IO.Put_Line ("Program failed: " & Exception_Message (E));
end File_Reader;

九、本课总结

  • Ada 预定义异常(Constraint_ErrorProgram_ErrorStorage_Error 等)覆盖常见运行时错误
  • 使用 exception 部分捕获异常,when 分支区分不同异常
  • raise 触发异常,可附带消息(Ada 2005+)
  • 未处理的异常沿调用链向上传播,直到被捕获或程序终止
  • 自定义异常扩展错误类型,提高代码可读性
  • 在子程序和包初始化中要小心处理异常,避免数据不一致

十、课后练习

  1. 基本练习:编写一个除法函数,捕获除零异常并返回 0.0

  2. 自定义异常:实现一个银行账户包,提款时若余额不足则触发 Insufficient_Funds 异常,并带消息显示当前余额。

  3. 异常传播:编写三个嵌套子程序,最深层抛出 Program_Error,中间层捕获并记录后重新抛出,最外层捕获并输出消息。

  4. 包初始化异常:创建一个包,在初始化时检查某个配置变量的值,若无效则触发异常。在主程序中捕获并处理。

  5. 文件处理:编写程序打开不存在的文件,捕获 Name_Error 并提示用户输入正确文件名。


十一、下节预告

第18课|输入输出详解

我们将:

  • 掌握 Ada.Text_IOAda.Integer_Text_IO 的使用
  • 学习文件输入输出的各种模式
  • 理解格式化输出与控制
  • 应用输入输出构建交互式程序

关键术语表

异常:程序运行时发生的错误或特殊情况

预定义异常:Ada 标准库中定义的常见异常(Constraint_Error 等)

exception:定义异常处理器或声明异常的关键字

raise:显式触发异常的关键字

when:在处理器中匹配特定异常的分支关键字

others:捕获所有未列出的异常的通配分支

Exception_Message:获取异常附带消息的函数(Ada.Exceptions

异常传播:异常未被处理时向调用者传递的过程

自定义异常:用户声明的异常,用于特定业务逻辑错误