Rust 错误处理

Rust的错误分两种:

  • 可恢复错误
  • 不可恢复错误

rust提供了可恢复错误的类型Result< T,E >,与不可恢复错误时终止运行的panic!宏。

不可恢复错误与panic!

程序会在panic!宏执行时打印出一段错误提示信息,展开并清理当前的调用栈,然后退出程序,这种情况大部分都发生在某个错误被检测到,但程序员却不知道该如何处理的时候。

panic的栈展开与终止
panic发生时,程序默认会开始栈展开,就是会沿着函数调用的反向顺序清理函数中的数据,为了支持这种操作,我们需要在二进制中存储许多额外信息。
除了栈展开我们还可以选择立即终止,让操作系统来进行回收工作。你可以在cargo.toml中的[profile]里设置panic=‘abort’来将panic的默认行为从栈展开切换为终止。

接下来我们尝试一下调用panic:

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

报错信息:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

显示了我们给的panic信息,以及panic位置。

使用panic!产生的回溯信息

略,只是展示了发生错误的回溯信息。

可恢复错误与Result

大部分错误其实都没有严重到需要整个程序停止运行的地步。例如,尝试打开文件的操作会因为文件不存在而失败。你也许会在这种情形下考虑创建新文件而不是终止程序。

Result枚举定义了两个变体——Ok和Err,如下所示:

#![allow(unused_variables)]
fn main() {
	enum Result<T, E> {
	    Ok(T),
	    Err(E),
	}
}

这里的T和E是泛型的参数。T代表了Ok变体中包含的值类型,该类型值会在执行成功时返回。E代表了Err变体中包含的错误类型,该类型值会在执行失败时返回。

我们现在来打开一个文件,它的返回值将是一个Result,我们需要使用match来处理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),
    };
}

Ok表示成功返回file,失败触发panic!。
file和error都是临时定义的变量。
通过=>操作符将变量返回给f。

匹配不同的错误

可以通过match error来确定错误类型,进而处理不同类型的错误。

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)
            }
        },
    };
}

这里的other_error是新建的临时变量,可以获得其余的错误类型。
另外,_占位符也可以获得其他错误,但是_不是变量,无法在后面使用。

更有经验的rust开发者可能会像下面这样实现:

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

fn main() {
    let f = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

这段代码使用了闭包,之后会讨论。它没有使用match,少了很多嵌套,更加易读。

失败时触发panic的快捷方式unwrap和expect

match处理Result很详尽,但有时太麻烦。可以使用unwrap方法来简化,在Ok时放回Ok内部值,在Err时调用panic!:

use std::fs::File;

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

加入不存在hello.txt文件,我们运行了这段代码,将会报出如下错误:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4

还有一个expect方法,它的作用是:它允许我们在unwrap的基础上指定panic!所附带的错误提示信息。使用expect并附带上一段清晰的错误提示信息可以阐明意图,更容易追踪到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: Error { repr: Os { code:
2, message: "No such file or directory" } }', src/libcore/result.rs:906:4

传播错误

有时编写的函数出现了错误,可以把错误返回给调用者,让他们决定应该如何做进一步的处理。
我们需要将返回值设定为Result,并明确什么时候返回Ok,什么时候返回Err。

#![allow(unused_variables)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.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),
    }
}
}

_占位符又出现了,因为这一次不需要再Ok里面声明变量,所以直接用 _占位符替代。

传播错误的快捷方式 :?运算符

#![allow(unused_variables)]
fn main() {
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)
}
}

?运算符可以快速地将错误返回。(unwrap是快速处理错误,?是快速返回错误)
假如这个Result的值是Ok,那么包含再Ok中的值就会作为这个表达式的结果返回,假如是Err,那么就会返回这个Err值。

不过,match表达式和?运算符的一个区别:
被?运算符所接收的错误会被隐式地被from函数处理,这个函数定义与From trait中,用于错误类型之间地转换。当?运算符调用from函数时,它就开始尝试将传入的错误类型转换为当前函数地返回错误类型。当一个函数拥有不同的失败原因,却使用了同一的错误返回类型来同事进行表达时,这个功能会十分有用。只要每个错误类型都实现了转换为返回错误类型的from函数,?运算符就会自动帮我们处理所有的转换过程。

?运算符消除了大量模板代码,我们甚至可以通过链式方法来调用进一步简化代码:

#![allow(unused_variables)]
fn main() {
	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)
	}
}

这个代码的作用和之前的代码功能一样,但代码量瞬间减少了。

?运算符只能用于返回Result的函数

?运算发只能用于返回Result的函数或实现了std::ops::Try的类型。(因为时返回错误的快捷方式啊,不能返回错误就没意义了)

另外,还有一种Result:

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

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

    Ok(())
}

这里的Box< dyn Error >被称为trait对象,之后讨论,你可以简单理解为“任何可能的错误类型”。

要不要使用panic!

只要你能确定自己可以代替调用者确定某种情形时不可恢复错误,就可以嗲用panic!。
如果你选择返回Result,你就将选择权交给了调用者。

对于某些不太常见的场景,直接触发panic!要比返回Result更合适。

示例、原型和测试

示例添加Result导致降低可读性,因为match代码太长了。
原型中往往无法确定错误处理方式,只需panic做标记
测试更应该暴露panic,即使这个方法不是需要测试的内容。

你比编译器拥有更多信息

当你能明确逻辑上不可能出错,就放心panic。这里使用unwrap直接处理。

fn main() {
    use std::net::IpAddr;

    let home: IpAddr = "127.0.0.1".parse().unwrap();
}

错误处理的指导原则

某个错误可能会使代码处于损坏状态,这时就使用panic。当某些非法的值,自相矛盾的值,或不存在的值被传入代码,且满足下列条件时,使用panic:

  • 虽坏状态并不包括于其中偶尔发生的事情。
  • 随后的代码无法在出现损坏状态后继续正常运行。
  • 没有合适的方法来将“处于损坏状态”这一信息编码至我们所使用的类型中。

如果错误时可预期的,应该使用Result而不是panic。

创建自定义类型来进行有效性验证

生命周期时一种泛型。普通泛型可以确保类型拥有期望的行为,生命周期能够确保引用在我们的使用过程中一直有效。 Rust的每个引用都有自己的生命周期,它对应着引用保持有效性的作用域。 在大多时候,生命周期都是隐式的且可以被推导 ...