- 异常处理
共有两种异常,一种是F#异常,一种是.NET异常,和之前提到的F#库和.NET库一样,看这个语言的视角不同,分法也就不一样。
F#需要用户定义后才能使用,.NET异常可以直接用。
所有.NET异常都继承其框架里面的异常基类Exception类,Exception类有很多属性:
- StackTrace属性 追踪错误发生位置,包括源文件和行号等信息。
- InnerException属性 若存在很多异常之间互有联系。例如,发生了a异常,导致了b异常,那么a异常就会被保存在b异常的这个属性中,我们可以查看。
- Message属性 提供异常的详细信息。
还有其余的,用到的时候可以翻阅相关资料。
-
使用try…with表达式处理代码错误
1.使用try捕获代码错误,产生一个异常对象。
2.使用with和模式匹配编写异常处理代码。try 表达式 with | 模式1 ->表示式1 | 模式2 ->表示式2
使用的时候需要注意两点:一是表达式返回值类型必须一致。二是若在with中没有找到相应的模式匹配,那么这个异常会向上传播,直到找到匹配的处理程序。
-
使用try…with处理.NET异常
let dividel x y = try Some (x/y) with | :?System.DivideByZeroException-> printf"除数为0!";None let result1 = dividel 100 0
//除数为0!val dividel : x:int -> y:int -> int option
val result1 : int option = None
- 代码里面有个Some函数,这不是我们自己写的,是系统函数
表明泛型转换成泛型,具体作用可能是专门防止异常发生。None是一个取值,F#直接把最后一个值当成表达式的最后返回值。 - 还有个:?符号,这个符号用来表明类型测试,测试是不是我们写好的类型。.NET类型需要用,F#类型不需要。
-
使用as关键字对.NET异常重命名
let divide2 x y = try Some (x/y) with | :?System.DivideByZeroException as ex -> printf"异常!%s"(ex.Message);None //调用Massage属性 let result2 = divide2 100 0
//异常!尝试除以零。val divide2 : x:int -> y:int -> int option
val result2 : int option = None -
对于同一个异常,使用when条件实现多种匹配
按照上面我们的方法,一个异常只能匹配一种模式,那么如何实现一种异常根据要求返回不同的值呢?let divide3 x y flag = try x/y with | ex when flag -> printfn"TRUE: %s"(ex.ToString());0 //ex是exn类型。根据flag取值不同,即使除数都是0,也返回不同的值。 | ex when not flag ->printfn"FLASE:%s"(ex.ToString());1 //这里的0和1是最后赋值的意思。 let result3 = divide3 100 0 true let result4 = divide3 100 0 false
//TRUE: System.DivideByZeroException: 尝试除以零。
在 FSI_0007.divide3(Int32 x, Int32 y, Boolean flag) 位置 C:\VisualStudio\Projects\myfsharp_test1\myfsharp1.fs:行号 3
FLASE:System.DivideByZeroException: 尝试除以零。
在 FSI_0007.divide3(Int32 x, Int32 y, Boolean flag) 位置 C:\VisualStudio\Projects\myfsharp_test1\myfsharp1.fs:行号 3
val divide3 : x:int -> y:int -> flag:bool -> int
val result3 : int = 0
val result4 : int = 1 -
上面讲的都是.NET异常,现在看看F#异常
使用if表达式对可能发生的错误进行预判,如果发生了错误,就生成异常,然后给异常处理程序处理。
我们生成异常的时候,有三种方式。我们用下面的三点分别讲解。 -
rasie函数
用rasie函数生成制定的异常类型,可以是F#异常,也可以是是.NET异常。
语法:rasie(异常类型表达式)let divide x y = if (y = 0) then raise(System.ArgumentException("除数不能为0!")) //System.ArgumentException("除数不能为0!")括号里面是传给该异常Massage属性的信息。 else x/y let result = divide 100 0
//System.ArgumentException: 除数不能为0!
在 FSI_0010.divide(Int32 x, Int32 y) 位置 C:\VisualStudio\Projects\myfsharp_test1\myfsharp1.fs:行号 3
在 <StartupCodeFSI0010>.FSI_0010>.FSI0010>.FSI_0010.main@() 位置 C:\VisualStudio\Projects\myfsharp_test1\myfsharp1.fs:行号 6
已因出错而停止 -
failwith函数
用failwith函数生成F#异常。
语法:failwith 串类型错误信息。串类型是文本字符串或者string类型的值,它将成为异常的Massage属性let divideFailwith x y = if (y = 0) then failwith"除数不能为0!" else x/y let testDivideFailwith x y = try divideFailwith x y with | Failure(msg)->printfn"%s" msg;0 let result = testDivideFailwith 100 0
//除数不能为0!
val divideFailwith : x:int -> y:int -> int
val testDivideFailwith : x:int -> y:int -> int
val result : int = 0
这段代码刚开始看有点莫名其妙,我是这么理解的:发生了错误,failwith就把它保存下来,(理解为动作)保存的不是错误,是发生了错误,保存我们怎么描述或者通知错误的话。这句话有个代号叫Failure,(理解为名词)Failure可以呈现我们的话。用专业的语句来说:Failure的功能是生成一个System.Exception异常对象,接收string类型的参数,也就是msg也就是Massage属性的取值,用来描述异常信息。
为什么非要多此一举用Failure呢?因为failewith生成了异常但是这个实例好的异常名称我们没办法获得,我们就使用Failure取这些实例好的异常的属性,就变相的分辨出了不同的异常。 -
invalidArg函数
用 invalidArg函数生成.NET异常。
语法: invalidArg 参数名 串类型错误信息_参数名是字符串,串类型是字符串或者string类型的值,它将成为异常的Massage属性_//定义国内7个法定节假日数组 let holidays = [|"元旦";"春节";"清明节";"端午节"; "劳动节";"中秋节";"国庆节";""|] //lookupHoliday的功能是给出下标,求出对应节日名称 let lookupHoliday festival = if (festival > 7 || festival < 1) then invalidArg "fastival" (sprintf"被传递的参数不在1--7的范围内,为%d。"festival) holidays.[festival - 1] let x1 = lookupHoliday 7 let x2 = lookupHoliday 1
//val holidays : string [] =
[|“元旦”; “春节”; “清明节”; “端午节”; “劳动节”; “中秋节”; “国庆节”; “”|]
val lookupHoliday : festival:int -> string
val x1 : string = “国庆节”
val x2 : string = “元旦”//定义国内7个法定节假日数组 let holidays = [|"元旦";"春节";"清明节";"端午节"; "劳动节";"中秋节";"国庆节";""|] //lookupHoliday的功能是给出下标,求出对应节日名称 let lookupHoliday festival = if (festival > 7 || festival < 1) then invalidArg "fastival" (sprintf"被传递的参数不在1--7的范围内,为%d。"festival) holidays.[festival - 1] let x3 = lookupHoliday 8
//System.ArgumentException: 被传递的参数不在1–7的范围内,为8。
参数名: fastival
在 FSI_0016.lookupHoliday(Int32 festival) 位置 C:\VisualStudio\Projects\myfsharp_test1\myfsharp1.fs:行号 7
在 <StartupCodeFSI0016>.FSI_0016>.FSI0016>.FSI_0016.main@() 位置 C:\VisualStudio\Projects\myfsharp_test1\myfsharp1.fs:行号 9
已因出错而停止
数组的定义暂时还没有讲到,这里先看着就可以,可以看到invalidArg函数直接根据参数生成相应的异常。
其实还可以复习,F#函数调用的时候直接跟着参数,看第八点,.NET库调用的时候一般都要加括号,看第九点。 -
异常处理方法:try…finally表达式
学过C++或者java的人可能对此有所了解,但是F#和他们是有区别的。
try…with和try…finally是两个不同的语句,不可以平行使用,必须嵌套使用。
try…with语句就是用try捕获异常,with处理异常。
try…finally语句执行try语句没有问题就直接执行然后执行finally结束,如果有问题,那么也不管问题是什么,系统自己一层层往上找错误,系统决定怎么处理就怎么处理,但是处理之前还是要执行finally,然后系统该干啥干啥,干完之后结束。
一般,finally中包含着一定会执行的代码,比如资源清理操作等等,它不像with那样给出异常语句,finally一般不包含具体的异常代码。let divide x y = let stream : System.IO.FileStream = System.IO.File.Create("test.txt") let writer: System.IO.StreamWriter = new System.IO.StreamWriter(stream) try writer.WriteLine("test1"); Some(x/y) finally writer.Flush() printfn"closing stream" stream.Close() let result = try divide 100 0 with | :?System.DivideByZeroException->printfn"exception handled.";None
//closing stream
exception handled.
val divide : x:int -> y:int -> int option
val result : int option = None
我们先来看看divide这段代码,首先尝试写入文本,在尝试计算x除y。
我们最后调用的时候,想计算100除0,这肯定会有异常,有了之后我们肯定会执行with语句的异常处理。
我们计算100除0的时候调用第一段的divide函数,这个函数写了一个test1文本,然后尝试运算x除y,尝试呀尝试,发现有问题,但是就是不管他,他知道系统会管的,**在系统管之前,把finally后面的代码执行了,**也就是刷新缓冲区,关闭数据流,关闭数据流。终于轮到系统处理那个异常了,怎么处理呢?系统一看有with,就直接用with处理了,打印一句处理完成结束。
下面是两个我们不熟悉的函数的解释:
-
自定义F#异常处理程序
语法:exception 异常名称 of 参数类型
异常名称是我们给这个异常起的名字,参数类型是我们提供这种参数类型才可能出发异常。exception Error1 of string //定义异常Error1,它包含一个string类型参数 exception Error2 of string * int //定义异常Error2,它包含一个string * int元组类型参数 //定义一个包含两个参数的函数 let function1 x y = try if x = y then raise(Error1("x")) //相等时出发Error1 else raise(Error2("x",10)) //不相等时出发Error2 with //异常匹配处理 | Error1(str) -> printfn"Error1 %s"str | Error2(str,i) -> printfn"Error2 %s %d"str i //测试实例 function1 101 101 function1 7 2
//Error1 x
//Error2 x 10
exception Error1 of string
exception Error2 of string * int
val function1 : x:'a -> y:'a -> unit when 'a : equality
val it : unit = ()
我们定义了一个函数来测试异常,两个参数相等时触发Error1,不相等时触发Error2。str只是一个形参,了里面的值是raise设置好的。
使用raise函数生成异常,with来匹配异常并处理 -
使用try…with和try…finally嵌套处理F#异常
exception InnerError of string exception OuterError of string let function1 x y = try try //使用rasie函数触发异常 if x = y then raise(InnerError("内部错误")) else raise (OuterError("外部错误")) with //使用模式匹配处理异常 | InnerError(str) ->printfn"输出错误提示1 %s" str finally printfn"总是输出该部分。" let function2 x y = try function1 x y with | OuterError(str) ->printfn"输出错误提示1 %s" str function2 100 100 function2 100 10
//输出错误提示1 内部错误
//总是输出该部分。
//总是输出该部分。
//输出错误提示1 外部错误
exception InnerError of string
exception OuterError of string
val function1 : x:'a -> y:'a -> unit when 'a : equality
val function2 : x:'a -> y:'a -> unit when 'a : equality
val it : unit = ()
- 调用function2当两个参数都100时,进入function1,先进入第一个try…finally那个匹配,然后进入第二个try…with匹配**产生内部错误异常1,打印。**无论如何finally都会执行,所以输出结果为:
输出错误提示1 内部错误
总是输出该部分。
其实function2自己也有个try…with但是没有匹配的模式,所以没有任何输出。 - 调用function2当一个参数是100,另一个参数是10时,进入function1,先进入第一个try…finally那个匹配,然后进入第二个try…with匹配**产生外部错误,但是没有相应的模式匹配处理这个错误,所以没有打印。**无论如何finally都会执行,所以输出结果为:
总是输出该部分。
现在来到function2自己的try…with,有相应的匹配模式,所以输出:
输出错误提示1 外部错误
如果两个都没有处理这个异常呢?按照之前的学习内容,就会一层一层向上寻找异常处理的程序,直到成功。
- Exception别名
我们知道,F#有两个库,一个.NET库,一个F#库。平常使用.NET库中的Exception类比较多,**我们把这个类在F#中做了重新定义,起了个别名叫exn类型。**也就是F#中的异常叫exn类型。
我们当然也可以把它定义成自己写的异常类名称:type exn = System.Exception
type x = System.Exception
这两种方法都可以。type exn = System.Exception type x = exn