Rust for cpp devs - 错误处理

Rust 将软件中的错误分为两个类型:可恢复错误和不可恢复错误。

对于可恢复错误,例如文件找不到,可以报告给调用者处理。而不可恢复错误,例如内存访问越界,则应该直接终止程序。

大部分语言并不区分这两者,而是统一使用 exception 机制。Rust 没有 exception,对于可恢复错误,Rust 使用 Result 作为返回值,对于不可恢复错误,用 panic! 宏终止程序。

不可恢复的错误使用 panic!

某些错误是调用者无法修复的,只能让程序终止。这时候我们使用 panic! 宏:

fn main() {
    panic!("crash and burn");
}

错误信息是:

thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Using a panic! Backtrace

我们常常遇到的情况是被调用者 panic,而非手动 panic。如:

fn main() {
    let v = vec![1,2,3];
    v[99];
}

结果是:

thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

提示我们可以通过 RUST_BACKTRACE 环境变量来跟踪到底什么触发了错误:

RUST_BACKTRACE=1 cargo run

可恢复错误使用 Result

对于使用方法错误(如用户输入的参数错误),使用 panic! 不是很合适。此外,大部分错误没有严重到需要让程序停止运行。这种情况下可以使用 Result 类型作为返回值:

enum Result {
    Ok(T),
    Err(E),
}

例如,如果我们要打开一个文件,需要处理文件不存在的情况:

use std::fs::File;

fn main() {
    let f = File::open("file.txt");
    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error)
    };
}

这时候如果路径下不存在 file.txt 这个文件,则会报错:

thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

语法糖:unwrapexpect

上面代码的 pattern 非常常见,因此 Result 类型定义了一些方法方法来减少开发者的劳动。

  • unwrap:如果 Ok,返回 Ok 中包含的值,如果 Err,调用 panic!
use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}
  • expect:类似于 unwrap,但是允许我们自定义提示信息。
use std::fs::File;

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

传递错误

有时我们希望将错误交给调用者处理,此时,我们就需要传递(propagating)这个错误。

use std::fs;
use std::io;
use std::io::Read;
use std::process;

fn read_username_from_file(filename: &str) -> Result {
    let f = fs::File::open(filename);

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

    let mut s = String::new();

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

    return result;
}

fn main() {
    let filename = "a.txt";
    match read_username_from_file(filename) {
        Ok(s) => println!("read from file {} succeeded, {}", filename, s),
        Err(e) => {
            println!("read from file {} failed: {}", filename, e);
            process::exit(1);
        }
    };
}

read_username_from_file 函数内部并不处理错误,而是通过返回一个 Result 类型来让调用者处理。在这里,错误类型是 io::Error,可能由 File::openread_to_string 方法引发。当返回错误时,调用者可以根据需求编写处理逻辑,例如使用默认用户名,或是像上面代码一样报错退出。

传递错误的语法糖:? 操作符

传递错误的逻辑非常常见,因此 Rust 提供了 ? 来简化代码。

Result 值后加 ? 的效果是:

  • Ok:返回 Ok 中的值,并继续执行
  • Err:跳出整个函数,并返回错误

因此,上面的函数可以简化为:

fn read_username_from_file(filename: &str) -> Result {
    let mut f = fs::File::open(filename)?;

    let mut s = String::new();

    f.read_to_string(&mut s)?;

    return Ok(s);
}

? 操作符已经省略了许多重复代码,它还支持链式调用,更加简洁:

fn read_username_from_file(filename: &str) -> Result {
    let mut s = String::new();

    fs::File::open(filename)?.read_to_string(&mut s)?;

    return Ok(s);
}

不指定错误类型

如果我们不关心错误类型,我们可以将io::Error 替换为 Box。它可以是任何实现了 Error trait 的类型,约等于所有的错误类型。

fn read_username_from_file(filename: &str) -> Result> {
    let mut s = String::new();

    fs::File::open(filename)?.read_to_string(&mut s)?;

    return Ok(s);
}

你可能感兴趣的:(Rust for cpp devs - 错误处理)