错误是软件中不可避免的一部分,Rust提供了多种机制来处理错误情况。本章总结了Rust的错误处理方式及其最佳实践。
Rust 将错误组合成两个主要类别:可恢复错误(*recoverable*)和 不可恢复错误(*unrecoverable*)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。
大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常,但是,有可恢复错误 Result<T, E> ,和不可恢复(遇到错误时停止程序执行)错误 panic!。
Rust将错误分为两大类:
与其他语言使用异常处理所有错误不同,Rust使用两种不同的机制处理这两类错误:
panic!宏Result<T, E>类型有两种方式会导致panic:
panic!宏示例:
fn main() {
    panic!("crash and burn");
}
输出:
thread 'main' panicked at src/main.rs:2:5: crash and burn
当panic发生时,Rust会打印错误信息、回溯信息,然后清理栈并退出。可以通过设置环境变量RUST_BACKTRACE=1获取更详细的回溯信息。
当panic发生时,Rust默认会进行展开(unwinding),即回溯栈并清理每个函数的数据。也可以选择立即终止(abort)程序,这样会让操作系统负责清理内存。
可以在Cargo.toml中配置panic行为:
[profile.release]
panic = 'abort'
Result<T, E>是一个枚举,定义如下:
enum Result<T, E> {
    Ok(T),   // 操作成功,包含成功值
    Err(E),  // 操作失败,包含错误信息
}
T和E是泛型参数,分别表示成功情况下的值类型和失败情况下的错误类型。
使用match表达式处理Result的例子:
use std::fs::File;
fn main() {
    let greeting_file_result = File::open("hello.txt");
    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("无法打开文件: {:?}", error),
    };
}
可以根据错误的类型采取不同的处理方式:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
    let greeting_file_result = File::open("hello.txt");
    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("无法创建文件: {:?}", e),
            },
            other_error => {
                panic!("无法打开文件: {:?}", other_error);
            }
        },
    };
}
unwrap是一个快捷方法,当Result为Ok时返回值,为Err时调用panic!:
let greeting_file = File::open("hello.txt").unwrap();
expect类似于unwrap,但允许指定panic错误信息:
let greeting_file = File::open("hello.txt")
    .expect("hello.txt应该包含在此项目中");
如果不想在函数中处理错误,可以将错误传播给调用者:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");
    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    let mut username = String::new();
    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}
?运算符简化了错误传播的代码:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}
可以进一步简化,链式调用:
fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();
    File::open("hello.txt")?.read_to_string(&mut username)?;
    Ok(username)
}
标准库的更简洁方法:
fn read_username_from_file() -> Result<String, io::Error> {
    std::fs::read_to_string("hello.txt")
}
?运算符只能用于返回类型与?作用对象兼容的函数中。主要有两种情况:
Result<T, E>,可以对Result使用?Option<T>,可以对Option使用?不能在返回类型为()的函数(如main)中使用?,除非修改其返回类型:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;
    Ok(())
}
panic!,如:
Result,如:
可以利用Rust的类型系统进行验证,创建自定义类型来确保值的有效性:
pub struct Guess {
    value: i32,
}
impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("猜测值必须在1到100之间,得到了 {}", value);
        }
        Guess { value }
    }
    pub fn value(&self) -> i32 {
        self.value
    }
}
这样,使用Guess类型的函数就可以确信它们接收到的值始终在1到100之间。
Rust的错误处理功能旨在帮助你编写更健壮的代码:
panic!宏表示程序处于无法处理的状态,让进程停止而不是尝试继续使用无效值Result枚举利用类型系统指示操作可能以可恢复的方式失败panic!和Result,你的代码在面对不可避免的问题时会更加可靠Rust的错误处理哲学是明确区分不同类型的错误并强制你在编译时处理它们,这使得程序更加健壮且易于维护。
好好学习,天天向上