跳轉至內容

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) 方法會丟擲三種類型的異常

ArgumentNullException

s 是空引用時發生。

FormatException

s 不表示數值輸入時發生。

OverflowException

s 表示的數字大於或小於 Int32.MaxValueInt32.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

Try/Finally

[編輯 | 編輯原始碼]

通常,異常會導致函式立即退出。但是,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()

use 語句

[編輯 | 編輯原始碼]

.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 繫結到異常物件值
上篇:輸入和輸出 索引 下篇:運算子過載
華夏公益教科書