F# 程式設計/異常處理
| F# : 異常處理 |
當程式遇到問題或進入無效狀態時,它通常會透過丟擲異常來響應。如果放任不管,未捕獲的異常會導致應用程式崩潰。程式設計師編寫異常處理程式碼來使應用程式從無效狀態中恢復。
讓我們看一下以下程式碼
let getNumber msg = printf msg; int32(System.Console.ReadLine())
let x = getNumber("x = ")
let y = getNumber("y = ")
printfn "%i + %i = %i" x y (x + y)
此程式碼在語法上是有效的,並且具有正確的型別。但是,如果我們提供錯誤的輸入,它可能會在執行時失敗
此程式輸出以下內容
x = 7 y = monkeys! ------------ FormatException was unhandled. Input string was not in a correct format.
字串 monkeys 不表示數字,因此轉換會因異常而失敗。我們可以使用 F# 的 try... with 來處理此異常,這是一種特殊的模式匹配結構
let getNumber msg =
printf msg;
try
int32(System.Console.ReadLine())
with
| :? System.FormatException -> System.Int32.MinValue
let x = getNumber("x = ")
let y = getNumber("y = ")
printfn "%i + %i = %i" x y (x + y)
此程式輸出以下內容
x = 7 y = monkeys! 7 + -2147483648 = -2147483641
當然,在一個 with 塊中捕獲多種型別的異常是完全可能的。例如,根據 MSDN 文件,System.Int32.Parse(s : string) 方法會丟擲三種類型的異常
- 當
s是空引用時發生。
- 當
s不表示數值輸入時發生。
- 當
s表示的數字大於或小於Int32.MaxValue或Int32.MinValue時發生(即該數字不能用 32 位有符號整數表示)。
我們可以透過新增額外的匹配案例來捕獲所有這些異常
let getNumber msg =
printf msg;
try
int32(System.Console.ReadLine())
with
| :? System.FormatException -> -1
| :? System.OverflowException -> System.Int32.MinValue
| :? System.ArgumentNullException -> 0
沒有必要對異常型別進行詳盡的匹配案例列表,因為未捕獲的異常將簡單地移到堆疊跟蹤中的下一個方法。
上面的程式碼演示瞭如何從無效狀態中恢復。但是,在設計 F# 庫時,通常很有用的是丟擲異常以通知使用者程式遇到了某種無效輸入。有幾個標準函式用於丟擲異常
(* General failure *)
val failwith : string -> 'a
(* General failure with formatted message *)
val failwithf : StringFormat<'a, 'b> -> 'a
(* Raise a specific exception *)
val raise : #exn -> 'a
(* Bad input *)
val invalidArg : string -> string -> 'a
例如
type 'a tree =
| Node of 'a * 'a tree * 'a tree
| Empty
let rec add x = function
| Empty -> Node(x, Empty, Empty)
| Node(y, left, right) ->
if x > y then Node(y, left, add x right)
else if x < y then Node(y, add x left, right)
else failwithf "Item '%A' has already been added to tree" x
通常,異常會導致函式立即退出。但是,finally 塊將始終執行,即使程式碼丟擲異常也是如此
let tryWithFinallyExample f =
try
printfn "tryWithFinallyExample: outer try block"
try
printfn "tryWithFinallyExample: inner try block"
f()
with
| exn ->
printfn "tryWithFinallyExample: inner with block"
reraise() (* raises the same exception we just caught *)
finally
printfn "tryWithFinally: outer finally block"
let catchAllExceptions f =
try
printfn "-------------"
printfn "catchAllExceptions: try block"
tryWithFinallyExample f
with
| exn ->
printfn "catchAllExceptions: with block"
printfn "Exception message: %s" exn.Message
let main() =
catchAllExceptions (fun () -> printfn "Function executed successfully")
catchAllExceptions (fun () -> failwith "Function executed with an error")
main()
此程式將輸出以下內容
------------- catchAllExceptions: try block tryWithFinallyExample: outer try block tryWithFinallyExample: inner try block Function executed successfully tryWithFinally: outer finally block ------------- catchAllExceptions: try block tryWithFinallyExample: outer try block tryWithFinallyExample: inner try block tryWithFinallyExample: inner with block tryWithFinally: outer finally block catchAllExceptions: with block Exception message: Function executed with an error
請注意,我們的 finally 塊在遇到異常的情況下仍然執行。finally 塊最常用於清理資源,例如關閉開啟的檔案控制代碼或關閉資料庫連線(即使在遇到異常的情況下,我們也不希望留下開啟的檔案控制代碼或資料庫連線)
open System.Data.SqlClient
let executeScalar connectionString sql =
let conn = new SqlConnection(connectionString)
try
conn.Open() (* this line can throw an exception *)
let comm = new SqlCommand(sql, conn)
comm.ExecuteScalar() (* this line can throw an exception *)
finally
(* finally block guarantees our SqlConnection is closed, even if our sql statement fails *)
conn.Close()
.NET 框架中的許多物件都實現了 System.IDisposable 介面,這意味著這些物件有一個名為 Dispose 的特殊方法,用於保證非託管資源的確定性清理。建議在不再需要這些型別的物件時立即呼叫它們的 Dispose 方法。
傳統上,我們會以這種方式使用 try/finally 塊
let writeToFile fileName =
let sw = new System.IO.StreamWriter(fileName : string)
try
sw.Write("Hello ")
sw.Write("World!")
finally
sw.Dispose()
但是,這有時會很笨拙和麻煩,尤其是在處理許多實現 IDisposable 介面的物件時。F# 提供了關鍵字 use 作為上述模式的語法糖。上面程式碼的等效版本可以寫成如下
let writeToFile fileName =
use sw = new System.IO.StreamWriter(fileName : string)
sw.Write("Hello ")
sw.Write("World!")
use 語句的作用域與 let 語句的作用域相同。當識別符號超出作用域時,F# 會自動呼叫物件的 Dispose() 方法。
F# 允許我們使用 exception 宣告輕鬆地定義新的異常型別。以下是一個使用 fsi 的示例
> exception ReindeerNotFoundException of string
let reindeer =
["Dasher"; "Dancer"; "Prancer"; "Vixen"; "Comet"; "Cupid"; "Donner"; "Blitzen"]
let getReindeerPosition name =
match List.tryFindIndex (fun x -> x = name) reindeer with
| Some(index) -> index
| None -> raise (ReindeerNotFoundException(name));;
exception ReindeerNotFoundException of string
val reindeer : string list
val getReindeerPosition : string -> int
> getReindeerPosition "Comet";;
val it : int = 4
> getReindeerPosition "Donner";;
val it : int = 6
> getReindeerPosition "Rudolf";;
FSI_0033+ReindeerNotFoundExceptionException: Rudolf
at FSI_0033.getReindeerPosition(String name)
at <StartupCode$FSI_0036>.$FSI_0036._main()
stopped due to error
我們可以像其他任何異常一樣輕鬆地對我們的新現有異常型別進行模式匹配
> let tryGetReindeerPosition name =
try
getReindeerPosition name
with
| ReindeerNotFoundException(s) ->
printfn "Got ReindeerNotFoundException: %s" s
-1;;
val tryGetReindeerPosition : string -> int
> tryGetReindeerPosition "Comet";;
val it : int = 4
> tryGetReindeerPosition "Rudolf";;
Got ReindeerNotFoundException: Rudolf
val it : int = -1
| 結構 | 種類 | 描述 |
raise expr
|
F# 庫函式 | 丟擲給定的異常 |
failwith expr
|
F# 庫函式 | 丟擲 System.Exception 異常 |
try expr with rules
|
F# 表示式 | 捕獲與模式規則匹配的表示式 |
try expr finally expr
|
F# 表示式 | 在計算成功和丟擲異常時都執行 finally 表示式 |
| :? ArgumentException
|
F# 模式規則 | 與給定的 .NET 異常型別匹配的規則 |
| :? ArgumentException as e
|
F# 模式規則 | 與給定的 .NET 異常型別匹配的規則,將名稱 e 繫結到異常物件值 |
| Failure(msg) -> expr
|
F# 模式規則 | 與給定的攜帶資料的 F# 異常匹配的規則 |
| exn -> expr
|
F# 模式規則 | 與任何異常匹配的規則,將名稱 exn 繫結到異常物件值 |
| exn when expr -> expr
|
F# 模式規則 | 在給定條件下匹配異常的規則,將名稱 exn 繫結到異常物件值 |