每个程序员都不能保证自己写的成员会按照理想状态去运行,就算是高手、大侠,也不能保证他的程序就是完美的。编写软件是一项复杂的事情,正式如此,所以即使是最好的软件也经常伴随着各种各样的问题,有的时候问题是糟糕的代码引起的(比如数组索引的溢出、未能判断空指针),有的时候未考虑用户数据的输入导致的(比如该输入数字时输入了字母),不管如何,任何引发的错误都影响到了程序的正常执行。
不过,对于.NET来说,它对于程序引发的异常提供了强大的处理能力,以其简单的语法就能处理复杂的异常,不像以前C或C++,处理起来比较复杂、混乱。
F#语言跟.NET其它语言一样,如果发生了异常,函数将立即退出,以及调用它的函数,直到异常处理捕获了异常,如果异常没有被捕获,程序将被终止。在F#中,一个简单的错误报告可以利用failwith函数来实现,failwith函数把一个字符串作为它的参数,并且调用它时,将引发一个System.Exception异常实例。failwith函数的另外一种版本是failwithf,它可以像printfn样,使用一个格式化的字符串来作为参数,比如:
let divide x y =
if y = 0 then
failwithf "Cannot divide %d by zero!" x
x / y
调用的时候输入参数10 0,将引发一个异常:
divide 10 0
System.Exception: Cannot divide 10 by zero!
at FSI_0003.divide(Int32 x, Int32 y)
at <StartupCode$FSI_0004>.$FSI_0004._main()
stopped due to error
上面抛出的异常类型跟其它的.NET语言一样,都包含了两个方面的信息,一个是异常信息,而另一个是堆栈跟踪。异常信息为程序员调试异常提供了帮助,而堆栈跟踪则为程序员提供了异常的出处。
除了利用failwith抛出一个简单的异常外,还可以利用raise函数来抛出特殊的、具体的异常信息。比如我们可以把上面的引发异常的地方更改为下面这样的:
let divide2 x y =
if y = 0 then raise <| new System.DivideByZeroException()
x / y
这样,当异常发生了,它提供了一个更具体的异常类型信息:
divide2 10 0
System.DivideByZeroException: Attempted to divide by zero.
at FSI_0005.divide2(Int32 x, Int32 y)
at <StartupCode$FSI_0007>.$FSI_0007._main()
stopped due to error
一、异常捕获
跟C#一样,在F#中可以利用try-catch表达式来捕获并处理异常。当一个异常在try-catch范围内被引发时,它将在with代码块内被处理,with是利用异常类型来进行模式匹配的。由于异常处理是由模式匹配来执行的,你可以结合使用或或者通配符来捕获任何的异常并处理。
如果在try-catch代码内,引发了一个异常,并且没有找到合适的异常处理,异常将继续往更高一层的结构抛出,知道被捕获或者终止。
try-catch表达式跟if表达式一样,也会返回一个值,所以try代码块内的最好一个表达式的值类型自然跟with块内的类型是一样的。
为了更好的说明异常处理机制,下面的代码段抛出了不同的异常类型,并且每一种可能的异常都相应的有一个适当的异常处理。
open System.IO
[<EntryPoint>]
let main (args : string[]) =
let exitCode =
try
let filePath = args.[0]
printfn "Trying to gather information about file:"
printfn "%s" filePath
let matchingDrive =
Directory.GetLogicalDrives()
|> Array.tryFind (fun drivePath -> drivePath.[0] = filePath.[0])
if matchingDrive = None then
raise <| new DriveNotFoundException(filePath)
let directory = Path.GetPathRoot(filePath)
if not <| Directory.Exists(directory) then
raise <| new DirectoryNotFoundException(filePath)
if not <| File.Exists(filePath) then
raise <| new FileNotFoundException(filePath)
let fileInfo = new FileInfo(filePath)
printfn "Created = %s" <| fileInfo.CreationTime.ToString()
printfn "Access = %s" <| fileInfo.LastAccessTime.ToString()
printfn "Size = %d" fileInfo.Length
0
with
| :? DriveNotFoundException
| :? DirectoryNotFoundException
-> printfn "Unhandled Drive or Directory not found exception"
1
| :? FileNotFoundException as ex
-> printfn "Unhandled FileNotFoundException: %s" ex.Message
3
| :? IOException as ex
-> printfn "Unhandled IOException: %s" ex.Message
4
| _ as ex
-> printfn "Unhandled Exception: %s" ex.Message
5
printfn "Exiting with code %d" exitCode
exitCode
异常的另外一种情况是可能没有合适的异常处理,导致异常没有被捕获,而程序又终止了。这样就有可能导致一些非托管资源没有被释放,如打开一个文件没有被正常的管被或者在写入文件时没有正常的清空缓冲区,所以,在异常处理上提供了第二种方式:try-finally表达式。在try-finally表达式中,finally内的代码无论是是否异常都会执行的,所以就提供了方便、快捷的方式来处理额外的事物,下面的代码描述了try-finally表达式的使用:
let tryFinallyTest() =
try
printfn "Before exception..."
failwith "ERROR!"
printfn "After exception raised..."
finally
printfn "Finally block executing..."
let test() =
try
tryFinallyTest()
with
| ex -> printfn "Exception caught with message: %s" ex.Message
二、再次引发异常
有时候,尽管你采取最大努力来纠正异常,但是也不能修复它,比如打开一个不存在的文件。在这种情况下,即使捕获了这个异常,但是还可以再次的抛出它,比如我们当打开一个不存在的文件时,引发了异常,然后在异常处理中需要记录错误日志,由于这个异常不能被修复,需要再次抛出。这时我们就像下面的代码样利用reraise函数:
open Checked
let reraiseExceptionTest() =
try
let x = 0x0fffffff
let y = 0x0fffffff
x * y
with
| :? System.OverflowException as ex
-> printfn "An overflow exception occured..."
reraise()
三、自定义异常类型
你可以像在C#中一样,利用继承System.Exception来自定义一个异常类型,然后出了这外,还可以使用轻量级异常语法以更简单的方式来自定义异常类型,它允许你像定义discriminated unions类型类似的语法来定义异常类型。下面的示例自定义了若干个异常类型,其中有的还关联了数据。这些轻量级异常的优点是,当它们被捕获后更容易获取相关数据,因为它们可以在discriminated unions类型中使用相同的语法来进行模式匹配,所以也没有必要使用:?>动态类型:
open System
open System.Collections.Generic
exception NoMagicWand
exception NoFullMoon of int * int
exception BadMojo of string
let castHex (ingredients : HashSet<string>) =
try
let currentWand = Environment.MagicWand
if currentWand = null then
raise NoMagicWand
if not <| ingredients.Contains("Toad Wart") then
raise <| BadMojo("Need Toad Wart to cast the hex!")
if not <| isFullMoon(DateTime.Today) then
raise <| NoFullMoon(DateTime.Today.Month, DateTime.Today.Day)
let mana =
ingredients
|> Seq.map (fun i -> i.GetHashCode())
|> Seq.fold (+) 0
sprintf "%x" mana
with
| NoMagicWand
-> "Error: A magic wand is required to hex!"
| NoFullMoon(month, day)
-> "Error: Hexes can only be cast during a full moon."
| BadMojo(msg)
-> sprintf "Error: Hex failed due to bad mojo [%s]" msg