一、前言
本章将要介绍的是Rust中的错误处理方式,这是一个与其它很多语言都非常不同的一点。
但在Rust的代码中,你也将会非常频繁的遇到它,所以为了更进一步学习Rust中的其它特性,这里有必要对Rust
中的错误处理进行学习。
二、不同之处
就目前来说,各类编程语言中主流的就两种错误形式:返回错误码,抛出异常。
其中,c语言就是典型采用的返回错误码方式,比如最常见的NULL
代表空指针等等,这一般就标志一个错误的产生。
至于C++中,错误码与异常都在用,不过大多数时候仍然是错误码用的比较多。
究其原因是异常带来的性能损耗,很多人不愿意承担,所以选择了性能更高、但不友好的返回错误码方式。
而在JAVA
这门语言中,绝大多数采用的都是抛出异常的方式。
java中有句笑话是,只要我将所有代码都try
起来,那就没有错误!
这两种处理错误的方式各有优缺点,异常的主要缺点是其带来的性能损耗,这一点是作为一门对标系统级开发语言的rust
所无法忍受的。
而如果采用返回错误码的方式(比如NULL
),作为以代码安全标榜自身的Rust
语言,同样无法接受。
但如果不采取这两种方式,那还有其它方式能完成这一目标吗?
有,那就是枚举!
三、枚举处理错误
那么枚举是如何处理错误的呢?我们可以直接看一个例子。
为了更加深刻的体会到枚举处理错误所带来的优势,我们首先使用C语言的方式,采用返回错误码来实现错误处理:
//写一个除法函数,传入 除数,返回100除以这个除数的结果
fn divi(i:u32) -> u32 {
if i==0{
//0不能作为除数,所以返回0作为错误发生
0
}else{
//正确,则返回运算的结果
100/i
}
}
这是一个非常简单的函数,传入一个除数,返回100
除以它的结果,如果这个除数为0
,就说明发生了错误,那就直接返回0作为错误码,否则返回正确的结果。
注意这里返回值的方式使用的是上一章提到的“表达式”,不理解的回去看一看。
那么在调用这个函数处理其结果时,一般就得像下面这样用:
fn main() {
let ret=divi(0);
if ret==0{
println!("函数调用出错");
}else{
println!("执行结果为:{}",ret);
}
}
在c语言中,我们平常写代码基本就是这样写的吧!
这样写可以吗?当然可以!只是不够安全!
因为如果你不看这个函数的使用方式,你就不知道它的错误码是多少,有些时候甚至你会直接忽略处理它的错误码、把它的结果当作正常返回值继续使用。
毕竟,它的返回值与错误码都是数字,根据墨菲定律,只要存在这种事情发生的可能性,那么它就已经会发生,这里同理,只要它可能被人滥用出错,那么就一定会出现滥用出错的事情。
不安全的代码行为,在rust中默认都是不被允许的,所以就出现了用枚举的方式来处理错误,现在我们再将上面的代码做一下处理:
enum Result{
Ok(u32),
Err(String)
}
//写一个除法函数,传入 除数,返回100除以这个除数的结果
fn divi(i:u32) -> Result {
if i==0{
//0不能作为除数,返回Err,并携带错误的原因
Result::Err("传入的参数不能为0".to_string())
}else{
//正确,返回Ok,并携带正常的结果
Result::Ok(100/i)
}
}
这次我们定义的是一个枚举类型Result
,它的内部有两个字段:Ok
与Err
。
其中的Ok
携带一个u32
的数据,而Err
则携带一个String
如果不理解的,请回头看看前面的章节:rust语法进阶。
此时关键点就来了,我们要让这个函数的返回值是这个枚举的类型Result
。
如果为0,那就返回Err
这个枚举量,并让它携带上错误的字符串信息。
如果为其它,那就返回Ok
这个枚举量,并让它携带上正确的执行结果。
这个时候,我们如果调用这个函数,就需要像下面这样做了:
fn main() {
let ret=divi(0);
match ret {
Result::Ok(r) => {
println!("执行的结果为:{}", r);
},
Result::Err(e) =>{
println!("{}",e);
}
}
}
因为返回值ret
此时是一个枚举Result
,所以我们就需要用match
来匹配它的结果。
由于result
中只有两种情况,所以内部的语句就有两个分支:Ok
一个分支,Err
一个分支。
并且注意,在分支处,我们都用一个变量来取出了它所携带的值,在VSCode
中,可以清晰的看到它的类型:
其中Ok
分支的值,携带的就是正确的执行结果,而Err
中携带的,则是错误的信息字符串。
然后就可以在对应的分支中进行各自的处理。
看到这里,我相信你已经感受到rust
中错误处理方式的妙处了:
- 由于
match
的特性,除非你显示的告诉它你不想匹配某些分支,否则你必须匹配并进行处理,这极大增强了代码的安全性,降低了程序员因疏忽错误处理导致的错误。 - 由于枚举的特性,它可以携带数据,因此它不仅可以判断执行正确与否,还能同时得到执行的结果,即使发生了错误,也能得到函数内部传出的错误信息。
是不是感觉这种处理方式特别妙!
也正因如此,rust
中所有的库,基本都采用的这种方式作为错误处理方式。
四、泛型
上面我们已经体会到了用枚举作为错误处理的方式,确实非常好用,但却仍然不够。
究其原因还是因为,上面的枚举Result
中的Ok
,已经被写死在代码中、只能携带u32
的数据类型。
如果我的另一个函数需要携带String
变量作为执行结果呢?难不成再写一个Result
?
但很明显这样是不行的,因为同名了
为了解决同名问题,你就必须重新取个名字,几个函数还好,如果是几十个函数呢?那可就太让人崩溃的。
所以这个时候,泛型就出现了,它的目的,就是解决我们取名这个问题。
它让我们可以用同一个Result
携带不同类型的数据,在我们眼中就只有一个Result
类型,而为携带不同类型的Result
取名的任务,就交给了rust
编译器。
事实上,这基本等价于C++中的模板,但更加强大、好用。
当然,泛型的作用远不止于此,但对于我们现在来说,泛型所展现出的功能已经足够了。