10. rust中的错误处理

一、前言

本章将要介绍的是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,它的内部有两个字段:OkErr

其中的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

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux