Rust语言圣经31 -返回值Result和?

Posted 编程学院

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Rust语言圣经31 -返回值Result和?相关的知识,希望对你有一定的参考价值。

本文节选自<<Rust语言圣经>>一书
欢迎大家加入Rust编程学院,一起学习交流:
QQ群:1009730433

可恢复的错误Result

还记得上一节中,提到的关于文件读取的思考题吧?当时我们解决了读取中如果遇到不可恢复错误该怎么处理,现在来看看,读取过程中,正常返回和遇到可以恢复的错误时该如何处理。

假设,我们有一台消息服务器,每个用户都通过websocket连接到该服务器来接收和发送消息,该过程就涉及到socket文件的读写,那么此时,如果一个用户的读写发生了错误,显然不能直接panic,否则服务器会直接崩溃,所有用户都会断开连接,因此我们需要一种更温和的错误处理方式: Result<T,E>.

之前章节有提到过,Result<T,E>是一个枚举类型,定义如下:

enum Result<T, E> 
    Ok(T),
    Err(E),

泛型参数T代表成功时存入的正确值,存放方式是Ok(T)E代表错误是存入的错误值,存放方式是Err(E),枯燥的讲解永远不及代码生动准确,因此先来看下打开文件的例子:

use std::fs::File;

fn main() 
    let f = File::open("hello.txt");

以上File::open返回一个Result类型, 那么问题来了:

如何获知变量类型或者函数的返回类型

有几种常用的方式:

  • 第一种是查询标准库或者三方库文档,搜索File,然后找到它的open方法,但是此处更推荐第二种方法:
  • Rust IDE章节,我们推荐了VSCode IED和rust-analyze插件,如果你成功安装的话,那么就可以在VScode中很方便的通过代码跳转的方式查看代码,同时rust-analyze插件还会对代码中的类型进行标注,非常方便好用!
  • 你还可以尝试故意标记一个错误的类型,然后让编译器告诉你:
let f: u32 = File::open("hello.txt");

错误提示如下:

error[E0308]: mismatched types
 --> src/main.rs:4:18
  |
4 |     let f: u32 = File::open("hello.txt");
  |                  ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
  |
  = note: expected type `u32`
             found type `std::result::Result<std::fs::File, std::io::Error>`

上面代码,故意将f类型标记成整形,编译器立刻不乐意了,你是在忽悠我吗?打开文件操作返回一个整形?来,大哥来告诉你返回什么:std::result::Result<std::fs::File, std::io::Error>,我的天呐,怎么这么长的类型!

别慌,其实很简单,首先Result本身是定义在std::result中的,但是因为Result很常用,就被包含在了prelude中(将常用的东东提前引入到当前作用域内),因此无需手动引入std::result::Result,那么返回类型可以简化为Result<std::fs::File,std::io::Error>,你看看是不是很像标准的Result<T,E>枚举定义?只不过T被替换成了具体的类型std::fs::File,是一个文件句柄类型,E被替换成std::io::Error,是一个IO错误类型.

这个返回值类型说明File::open调用如果成功则返回一个可以进行读写的文件句柄,如果失败,则返回一个IO错误: 文件不存在或者没有访问文件的权限等。总之File::open需要一个方式告知调用者是成功还是失败,并同时返回具体的文件句柄(成功)或错误信息(失败), 万幸的是,这些信息Result枚举可以提供:

use std::fs::File;

fn main() 
    let f = File::open("hello.txt");

    let f = match f 
        Ok(file) => file,
        Err(error) => 
            panic!("Problem opening the file: :?", error)
        ,
    ;

代码很清晰,对打开文件后的Result<T,E>类型进行匹配取值,如果是成功,则将Ok(file)中存放的的文件句柄file赋值给f,如果失败,则将Err(error)中存放的错误信息error使用panic抛出来,进而结束程序,这非常符合上文提到过的panic使用场景。

好吧,也没有那么合理:)

对返回的错误进行处理

直接panic还是过于粗暴,因为实际上IO的错误有很多种,我们需要对部分错误进行特殊处理,而不是所有错误都直接崩溃:

use std::fs::File;
use std::io::ErrorKind;

fn main() 
    let f = File::open("hello.txt");

    let f = match f 
        Ok(file) => file,
        Err(error) => match error.kind() 
            ErrorKind::NotFound => match File::create("hello.txt") 
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: :?", e),
            ,
            other_error => panic!("Problem opening the file: :?", other_error),
        ,
    ;

上面代码在匹配出error后,又对error进行了详细的匹配解析,最终结果:

  • 如果是文件不存在错误ErrorKind::NotFound,就创建文件,这里创建文件File::create也是返回Result,因此继续用match对其进行处理:创建成功,将新的文件句柄赋值给f,如果失败,则panic
  • 剩下的错误,一律panic

虽然很清晰,但是代码还是有些啰嗦,我们会在简化错误处理一章重点讲述如何写出更优雅的错误.

失败就panic: unwrap和expect

上一节中,已经看到过这两兄弟的简单介绍,这里再来回顾下。

在不需要处理错误的场景,例如写原型、示例时,我们不想使用match去匹配Result<T,E>以获取其中的T值,因为match的穷尽匹配特性,你总要去处理下Err分支。那么有没有办法简化这个过程?有,答案就是unwrapexpect

它们的作用就是,如果返回成功,就将Ok(T)中的值取出来,如果失败,就直接panic,真的勇士决不多BB,直接崩溃.

use std::fs::File;

fn main() 
    let f = File::open("hello.txt").unwrap();

如果调用这段代码时不存在 hello.txt 文件,那么unwrap就将直接panic

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os  code: 2, kind: NotFound, message: "No such file or directory" ', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

expectunwrap很像,只不过它允许指定panic!时的报错信息:

use std::fs::File;

fn main() 
    let f = File::open("hello.txt").expect("Failed to open hello.txt");

报错如下

thread 'main' panicked at 'Failed to open hello.txt: Os  code: 2, kind: NotFound, message: "No such file or directory" ', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

可以看出,expect相比unwrap能提供更精确的错误信息,在有些场景也会更加实用。

传播错误

咱们的程序几乎不太可能只有A->B形式的函数调用,一个设计良好的程序,一个功能涉及十来层的函数调用都有可能。而错误处理也往往不是哪里调用出错,就在哪里处理,实际应用中,大概率会把错误层层上传然后交给调用链的上游函数进行处理,由此,错误传播将极为常见.

例如以下函数从文件中读取用户名,然后将结果进行返回:

use std::fs::File;
use std::io::self, Read;

fn read_username_from_file() -> Result<String, io::Error> 
    // 打开文件,f是`Result<文件句柄,io::Error>`
    let f = File::open("hello.txt");

    let mut f = match f 
        // 打开文件成功,将file句柄赋值给f
        Ok(file) => file,
        // 打开文件失败,将错误返回(向上传播)
        Err(e) => return Err(e),
    ;
    // 创建动态字符串s
    let mut s = String::new();
    // 从f文件句柄读取数据并写入s中
    match f.read_to_string(&mut s) 
        // 读取成功,返回Ok封装的字符串
        Ok(_) => Ok(s),
        // 将错误向上传播
        Err(e) => Err(e),
    

有几点值得注意:

  • 该函数返回一个Result<String, io::Error>类型,当读取用户名成功时,返回Ok(String),失败时,返回Err(io:Error)
  • File::openf.read_to_string返回的Result<T,E>中的E就是io::Error

由此可见,该函数将io::Error的错误往上进行传播, 该函数的调用者最终会对Result<String,io::Error>进行再处理,至于怎么处理就是调用者的事,如果是错误,它可以选择继续向上传播错误,也可以直接panic,亦或将具体的错误原因包装后写入socket中呈现给终端用户。

但是上面的代码也有自己的问题,那就是太长了(优秀的程序员身上的优点极多,其中最大的优点就是),我自认为也有那么一点点优秀,因此见不到这么啰嗦的代码,下面咱们来讲讲如何简化它。

传播界的大明星: ?

大明星出场,必需得有排面,来看看?的排面:

use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> 
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)

看到没,这就是排面,相比前面的match处理错误的函数,代码直接减少了一半不止,但是,一山更比一山难,看不懂啊!

其实?就是一个宏,它的作用跟上面的match几乎一模一样:

let mut f = match f 
    // 打开文件成功,将file句柄赋值给f
    Ok(file) => file,
    // 打开文件失败,将错误返回(向上传播)
    Err(e) => return Err(e),
;

如果结果是Ok(T),则把T赋值给f,如果结果是Err(E),则返回该错误,所以?特别适合用来传播错误。

虽然?match功能一致,但是事实上?会更胜一筹。何解?

想象一下,一个设计良好的系统中,肯定有自定义的错误特征,错误之间很可能会存在上下级关系,例如标准库中的std::io::Errorstd::error::Error,前者是io相关的错误结构体,后者是一个最最通用的标准错误特征,同时前者实现了后者,因此std::io::Error可以转换为std:error::Error

明白了以上的错误转换,?的更胜一筹就很好理解了,它可以自动进行类型提升:

fn open_file() -> Result<File, Box<dyn std::error::Error>> 
    let mut f = File::open("hello.txt")?;
    Ok(f)

上面代码中File::open报错时返回的错误是std::io::Error类型,但是open_file函数返回的错误类型是std::error::Error的特征对象,可以看到一个错误类型通过?返回后,变成了另一个错误类型,这就是?的神奇之处。

根本原因是在于标准库中定义的From特征,该特征有一个方法from,该方法用于把一个类型转成另外一个类型,?可以自动调用该方法,然后进行隐式类型转换。因此只要函数返回的错误ReturnError实现了From<OtherError>特征,那么?就会自动把OtherError转换为ReturnError

这种转换非常好用,意味着你可以用一个大而全的ReturnError来覆盖所有错误类型,只需要为各种子错误类型实现这种转换即可。

强中自有强中手,一码更比一码短:

use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> 
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)

瞧见没? ?还能实现链式调用,File::open遇到错误就返回,没有错误就将Ok中的值取出来用于下一个方法调用,简直太精妙了,从Go语言过来的我,内心狂喜(其实学Rust的苦和痛我才不会告诉你们)。

不仅有更强,还要有最强,我不信还有人比我更短((不要误解):

use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> 
    // read_to_string是定义在std::io中的方法,因此需要在上面进行引用
    fs::read_to_string("hello.txt")

从文件读取数据到字符串中,是比较常见的操作,因此Rust标准库为我们提供了fs::read_to_string函数,该函数内部会打开一个文件、创建String、读取文件内容最后写入字符串并返回,因为该函数无法帮助我们学习该章的内容,因此放在最后来讲,其实只是我想震你们一下:)

?用于Option的返回

?不仅仅可以用于Result的传播,还能用于Option的传播,再来回忆下Option的定义:

pub enum Option<T> 
    Some(T),
    None

Result通过?返回错误,那么Option就通过?返回None

fn first(arr: &[i32]) -> Option<&i32> 
   let v = arr.get(0)?;
   Some(v)

上面的函数中,arr.get返回一个Option<&i32>类型,因为?的使用,如果get的结果是None,则直接返回None,如果是Some(&i32),则把里面的值赋给v

其实这个函数有些画蛇添足,我们完全可以写出更简单的版本:

fn first(arr: &[i32]) -> Option<&i32> 
   arr.get(0)

有一句话怎么说?没有需求,制造需求也要上。。。大家别跟我学习,这是软件开发大忌. 只能用代码洗洗眼了:

fn last_char_of_first_line(text: &str) -> Option<char> 
    text.lines().next()?.chars().last()

上面代码展示了在链式调用中使用?提前返回None的用法,.next方法返回的是Option类型:如果返回Some(&str),那么继续调用chars方法,如果返回None,则直接从整个函数中返回None,不再继续进行链式调用.

新手用?常会犯的错误

初学者在用?时,老是会犯错,例如写出这样的代码:

fn first(arr: &[i32]) -> Option<&i32> 
   arr.get(0)?

这段代码无法通过编译,切记:?操作符需要一个变量来承载正确的值,只有错误值能直接返回,正确的值不行。因此?只能用于以下形式:

  • let v = xxx()?;
  • xxx()?.yyy()?;

带返回值的main函数

因为刚才讲的?使用限制,这段代码你很容易看出它无法编译:

use std::fs::File;

fn main() 
    let f = File::open("hello.txt")?;

因为?要求Result<T,E>形式的返回值,而main函数的返回是(),因此无法满足,那是不是就无解了呢?

实际上Rust还支持另外一种形式的main函数:

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> 
    let f = File::open("hello.txt")?;

    Ok(())

这样就能使用?提前返回了,同时我们又一次看到了Box<dyn Error>特征对象,因为std::error:Error是Rust中抽象层次最高的错误,其它标准库中的错误都实现了该特征,因此我们可以用该特征对象代表一切错误,就算main函数中调用任何标准库函数发生错误,都可以通过Box<dyn Error>这个特征对象进行返回.

至于main函数可以有多种返回值,那是因为实现了[std::process::Termination]特征,目前为止该特征还没进入稳定版Rust中,也许未来你可以为自己的类型实现该特征!

至此,Rust的基础内容学习已经全部完成,下面我们将学习Rust的高级进阶内容,正式开启你的高手之路。

以上是关于Rust语言圣经31 -返回值Result和?的主要内容,如果未能解决你的问题,请参考以下文章

Rust语言圣经27 - 深入了解特征

Rust语言圣经18 - 数组

Rust语言圣经23 - 方法Method

Rust语言圣经32 - 动态数组Vec

Rust语言圣经30 - Panic原理剖析

Rust语言圣经17 - 枚举enum