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
loopP :=newInt_Node'(0, P);endloop; -- Soon raises Storage_Error, -- because of the extreme memory leak.
例 3 比較以下方法
procedureCompute_Sqrt (X :inFloat; Sqrt :outFloat; OK :outBoolean)isbeginifX >= 0thenOK := True; -- compute √X ...elseOK := False;endif;endCompute_Sqrt; ...procedureTriangle (A, B, C :inFloat; Area, Perimeter :outFloat; Exists :outBoolean)isS :constantFloat := 0.5 * (A + B + C); OK : Boolean;beginCompute_Sqrt (S * (S-A) * (S-B) * (S-C), Area, OK); Perimeter := 2.0 * S; Exists := OK;endTriangle;
對 Compute_Sqrt 的負引數會導致 OK 設定為 False。Triangle 使用它來確定其自身狀態引數值,依此類推,一直到呼叫樹的頂端,ad nauseam。
對比
functionSqrt (X : Float)returnFloatisbeginifX < 0.0thenraiseConstraint_Error;endif; -- compute √X ...endSqrt; ...procedureTriangle (A, B, C :inFloat; Area, Perimeter :outFloat)isS:constantFloat := 0.5 * (A + B + C);beginArea := Sqrt (S * (S-A) * (S-B) * (S-C)); Perimeter := 2.0 * S;endTriangle;
對 Sqrt 的負引數會導致 Constraint_Error 在 Sqrt 內部顯式引發,並傳播出去。Triangle 只是傳播異常(不處理它)。
或者,我們可以使用型別系統來捕獲錯誤
subtypePos_FloatisFloatrange0.0 .. Float'Last;functionSqrt (X : Pos_Float)returnPos_Floatisbegin-- compute √X ...endSqrt;
對 Sqrt 的負引數現在會在呼叫點引發 Constraint_Error。Sqrt 甚至從未進入。
預定義包 Ada.Text_IO 的子程式引發的一些異常示例如下:
- End_Error,當 Get、Skip_Line 等在已達到檔案末尾時引發。
- Data_Error,當 Get 在 Integer_IO 等中引發,如果輸入不是預期的型別的字面量。
- Mode_Error,當嘗試從輸出檔案讀取或向輸入檔案寫入時引發,等等。
- Layout_Error,當在文字 I/O 操作中指定無效資料格式時引發
例 1
declareA : Matrix (1 .. M, 1 .. N);beginforIin1 .. MloopforJin1 .. NloopbeginGet (A(I,J));exceptionwhenData_Error => Put ("Ill-formed matrix element"); A(I,J) := 0.0;end;endloop;endloop;exceptionwhenEnd_Error => Put ("Matrix element(s) missing");end;
異常的宣告方式與物件類似。
例 1 聲明瞭兩個異常
Line_Failed, Line_Closed: exception;
但是,異常不是物件。例如,對聲明瞭異常的範圍的遞迴重新進入不會建立相同名稱的新異常;相反,會重複使用外部呼叫中宣告的異常。
例 2
packageDirectory_EnquiriesisprocedureInsert (New_Name :inName; New_Number :inNumber);procedureLookup (Given_Name :inName; Corr_Number :outNumber); Name_Duplicated :exception; Name_Absent :exception; Directory_Full :exception;endDirectory_Enquiries;
當發生異常時,正常的執行流程將被放棄,異常將被傳遞到呼叫序列中,直到找到匹配的處理程式。任何宣告區域(除了包規範)都可以有處理程式。處理程式指定它將處理的異常。透過向上移動呼叫序列,異常可以變為匿名;在這種情況下,它們只能由以下處理程式進行處理:others 處理程式。
functionFreturnSome_Typeis... -- declarations (1)begin... -- statements (2)exception-- handlers start here (3)whenName_1 | Name_2 => ... -- The named exceptions are handled with these statementswhenothers=> ... -- any other exceptions (also anonymous ones) are handled hereendF;
在宣告區域本身 (1) 引發的異常 (1) 不能由該區域的處理程式 (3) 處理;它們只能在外部作用域中進行處理。在語句序列 (2) 中引發的異常當然可以在 (3) 中進行處理。
這條規則的原因是,處理程式可以假設在宣告區域 (1) 中宣告的任何專案都是定義良好的,並且可以被引用。如果 (3) 處的處理程式可以處理在 (1) 處引發的異常,則將不知道哪些專案存在,哪些專案不存在。
raise 語句顯式引發指定異常。
例 1
packagebodyDirectory_EnquiriesisprocedureInsert (New_Name :inName; New_Number :inNumber)is…begin…ifNew_Name = Old_Entry.A_NamethenraiseName_Duplicated;endif; … New_Entry :=newDir_Node'(New_Name, New_Number,…); …exceptionwhenStorage_Error =>raiseDirectory_Full;endInsert;procedureLookup (Given_Name :inName; Corr_Number :outNumber)is…begin…ifnotFoundthenraiseName_Absent;endif; …endLookup;endDirectory_Enquiries;
異常處理程式可以分組在塊、子程式體等的末尾。處理程式是任何可能以以下方式結束的語句序列:
- 透過完成;
- 透過執行return 語句;
- 透過引發另一個異常 (raise e;);
- 透過重新引發相同異常 (raise;)。
假設在語句序列U(塊、子程式體等)中引發了異常e。
- 如果U 包含e 的處理程式:將執行該處理程式,然後控制權將離開U。
- 如果U 不包含e 的處理程式:e 將被傳播到U 的外部;實際上,e 在U 的“呼叫點”處引發。
因此,引發異常會導致導致異常發生的語句序列在異常發生點被放棄。它不會也不可能恢復。
例 1
...exceptionwhenLine_Failed =>begin-- attempt recovery Log_Error; Retransmit (Current_Packet);exceptionwhenLine_Failed => Notify_Engineer; -- recovery failed! Abandon_Call;end; ...
Ada 在 Ada.Exceptions 中提供有關異常的資訊,該資訊位於型別 Exception_Occurrence 的物件中,以及以該型別作為引數的子程式
- Exception_Name: 使用點表示法並以大寫字母返回完整的異常名稱。例如:Queue.Overflow.
- Exception_Message: 返回與事件相關的異常訊息。
- Exception_Information: 返回包含異常名稱和相關異常訊息的字串。
要獲取異常事件物件,可以使用以下語法
withAda.Exceptions;useAda.Exceptions; ...exceptionwhenError: High_Pressure | High_Temperature => Put ("Exception: "); Put_Line (Exception_Name (Error)); Put (Exception_Message (Error));whenError:others=> Put ("Unexpected exception: "); Put_Line (Exception_Information(Error));end;
當異常訊息內容未由引發異常的使用者設定時,它是實現定義的。它通常包含異常的原因和引發位置。
使用者可以使用 Raise_Exception 過程指定訊息。
declareValve_Failure :exception;begin... Raise_Exception (Valve_Failure'Identity, "Failure while opening"); ... Raise_Exception (Valve_Failure'Identity, "Failure while closing"); ...exceptionwhenFail: Valve_Failure => Put (Exception_Message (Fail));end;
從 Ada 2005 開始,可以使用更簡單的語法將字串訊息與異常事件關聯。
-- This language feature is only available from Ada 2005 on.declareValve_Failure :exception;begin...raiseValve_Failurewith"Failure while opening"; ...raiseValve_Failurewith"Failure while closing"; ...exceptionwhenFail: Valve_Failure => Put (Exception_Message (Fail));end;
Ada.Exceptions 包還提供用於儲存異常事件和重新引發它們的子程式。
- 第 4 章:程式結構
- 第 5 章:程式設計實踐
- 第 7 章:可移植性
