一、前言
本章将要介绍的是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++中的模板,但更加强大、好用。
当然,泛型的作用远不止于此,但对于我们现在来说,泛型所展现出的功能已经足够了。
下面我们就来看看泛型是如何做到的:
enum Result<T>{
Ok(T),
Err(String)
}
泛型的使用方式非常的简单,就是在枚举名称的后面添加尖括号<>
,然后在其中写一个名字,一般我们会写T
,即Type
的意思。
此时,这个T代表的就是一种类型,比如它可以为u32
,也可以为String
,它可以为任何其它类型。
因此只要让Ok
中要携带的类型写为T
即可,这样我们一个Result
就可以携带不同类型的数据了,使用方式如下:
fn divi(i:u32) -> Result<u32> {
if i==0{
//0不能作为除数
Result::Err("传入的参数不能为0".to_string())
}else{
//正确,则返回运算的结果
Result::Ok(100/i)
}
}
可以看到,其它的代码与上面相比并没有任何的变化,唯一有变化的就是需要在返回值处的Result
后面,写上它要携带的数据类型。
比如这里我们想要携带的是u32
,这时它内部的T就会被编译器替换为u32
,于是Ok
携带的就是u32
类型的数据了。
除此之外,我们无需做任何变动,代码就能正常运行。
实际上,真正的写法是这样:Result::<u32>::Ok(100/i)
。
也就是在使用这个Ok
字段时(或者Err
字段),还要写上一个::<u32>
来显式声明这个Result
中的T
是个什么类型。
但由于Rust
的编译器非常强大,可以自动为我们推导出它的类型,所以大部分时候我们是不用写的。
并且最重要的是,它带来了一个非常大的好处,那就是我们可以携带任意类型的变量了
Result::Ok("string".to_string());
Result::Ok(true);
Result::Ok(3.1415);
是不是特别好用!
五、标准错误处理
通过上面的讲解,你应该已经理解了Rust
中对于错误的处理方式与逻辑。
所以接下来我们再来看一看标准的处理错误方式。
事实上,它的处理方式与我们上面讲解的基本一模一样。
1.Result
正因为这种错误处理的方式基本一致,所以标准库自己推出了标准的Result
。