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 绑定到异常对象值 |