跳轉到內容

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 質量和風格指南

[編輯 | 編輯原始碼]
華夏公益教科書