Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

9.3 Result枚举与可恢复的错误 Pt.2:传播错误、问号运算符与链式调用

9.3.1 传播错误

当你编写的函数中包含了一些可能会执行失败的调用时,除了在函数里处理这个错误,还可以把错误返回给调用者,让它来决定如何进一步处理这个错误。

看个例子:

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

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("6657.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

fn main() {
    let result = read_username_from_file();
}

这段代码的意图是从文件中读取用户名:

  • 它的返回类型是Result枚举。它的两个类型参数TE对应Stringio::Error。也就是说,当一切顺利时,函数会返回Result下的Ok变体,Ok里包裹着String类型的用户名;如果遇到了问题,这个函数就会返回Result下的Err变体,在这个变体里会包含io::Error的实例。

  • 再看函数体:首先使用File::open尝试打开一个文件,把Result赋给f,然后对f进行match操作(这里把第二个f设为可变,是因为下文的read_to_string会使用&mut self)。如果操作成功,会返回file并把值赋给f;如果操作失败,就会return Err(e)。这里的e就是具体发生的错误,而在函数体里面遇到return关键字就表示函数执行到此为止,返回return后面的值,也就是Err(e)。错误类型恰好是io::Error,所以返回值符合Result的类型参数。

  • 如果File::open操作成功,接下来函数就创建了一个可变的String,叫s,然后调用read_to_string把文件里的内容读取到s里面。当然read_to_string也可能会失败,所以后面还跟了一个match表达式。

  • 这个match表达式结尾没有分号,它也是这个函数的最后一个表达式,所以它就是这个函数的返回值。这个match有两个分支:如果操作成功,就返回ResultOk变体,并且把String类型的s封装到里面;如果操作失败,就返回Err变体,把错误e包裹在里面返回。而read_to_string的返回错误类型恰好也是io::Error,所以返回值符合Result的类型参数。

9.3.2 ?运算符

在Rust里传播错误非常常见,所以Rust专门提供了?运算符来简化这个过程。

使用?实现上文例子的同样效果:

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

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

fn main() {
    let result = read_username_from_file();
}
  • 对于第一个?(第5行):File::open返回的是Result,加上?的意思是:如果File::open返回Ok,那么包裹在Ok里的值就会作为表达式的结果赋给f;如果File::open返回Err,那么就会终止函数的执行,把Err及里面包裹的错误信息作为整个函数的返回值返回——也就是return Err(e)。换句话说,第5行代码的效果等同于:
#![allow(unused)]
fn main() {
let f = File::open("6657.txt");
let mut f = match f {
    Ok(file) => file,
    Err(e) => return Err(e),
};
}
  • 对于第二个?(第7行):如果read_to_string操作成功,就会继续往下执行。成功的返回值实际上在代码中没有用到;而如果执行失败,就会终止函数的执行,把Err及里面包裹的错误信息作为整个函数的返回值返回——也就是return Err(e)

  • 如果前面都操作成功,那么表达式Ok(s)会把String类型的s包裹在Ok里返回。

总结一下:把?用于Result时,如果是Ok,那么Ok中的值就是表达式的结果,然后程序继续执行;如果操作失败,也就是Err,那么Err就是整个函数的返回值,就像使用了return

9.3.3 ?from函数

Rust提供了from函数。它来自std::convert::From这个trait,作用是在错误之间进行转换,将一个错误类型转化为另外一个错误类型。被?所接收的错误会隐式地被from处理:from会查看当前函数应当返回的错误类型是什么,然后转换为什么。

就以刚才的代码为例,read_username_from_file的返回值是Result<String, io::Error>,所以from看得出来函数需要io::Error作为发生错误时的返回类型,就会把不同的错误类型转化为io::Error。这里只是碰巧函数体内的所有错误类型都已经是io::Error,所以不需要转化这一步。

这个特点在需要把不同的错误原因映射成同一种错误类型时非常有用。前提条件是涉及到的错误类型实现了From trait,从而可以转换为所返回的错误类型。

9.3.4 链式调用

其实之前的例子还可以继续优化,就是使用链式调用。优化后的代码如下:

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

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

fn main() {
    let result = read_username_from_file();
}

刚刚说过了,把?用于Result时,如果是Ok,那么Ok中的值就是表达式的结果,然后程序继续执行。那就可以消除原代码中赋值的步骤,直接使用链式调用。

9.3.5 ?只能用于返回ResultOption的函数

看个例子:

use std::fs::File;
fn main() {
    let result = File::open("6657.txt")?;
}

输出:

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:3:40
  |
2 | fn main() {
  | --------- this function should return `Result` or `Option` to accept `?`
3 |     let result = File::open("6657.txt")?;
  |                                        ^ cannot use the `?` operator in a function that returns `()`
  |
help: consider adding return type
  |
2 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
3 |     let result = File::open("6657.txt")?;
4 +     Ok(())
  |

报错内容是:?运算符只能用于返回值是ResultOption这类实现了Try trait的类型,而main的返回类型是(),也就是单元类型,相当于什么也没返回。

但是,谁说main的返回类型一定是单元类型呢?只要把它的返回类型改成Result不就解决了吗?

代码如下:

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

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

    Ok(())
}
  • 把返回类型改为Result<(), Box<dyn Error>>,也就是说:如果程序正常运行,会返回Ok变体,里面包裹着单元类型;如果没有正常运行,会返回Err变体,包裹着Box<dyn Error>(其中的Errorstd::error::Error)。这是一个trait对象,以后会讲,这里可以把它简单地理解为任何可能的错误类型。

  • 如果能成功读取文件,那么?就会把包裹在Ok里的文件数据返回赋给result,然后继续执行。Ok(())main里的最后一个表达式,它返回Ok变体,同时把单元类型包裹着。

  • 如果不能成功读取,那么?就会把Err(e)作为main的返回值返回回去,并且函数执行到此结束。