跳转至内容

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 绑定到异常对象值
上篇:输入和输出 索引 下篇:运算符重载
华夏公益教科书