拉上Java 来谈谈 Rust的错误处理

1 前言

每个语言都会有异常处理机制(没有异常处理机制的语言估计也没有人会用了),Rust 自然也不例外,所以今天我就来谈Rust 的异常处理,因为 Rust 的异常处理跟常见的语言 (Java/Python 等)的处理机制差异略大,所以打算拉个上个语言,对比着解释. 没错,这 个光荣的任务就落到了 Java 身上

2 Java 的异常处理

在谈 Rust 的异常处理之前,为了把它们之前的差异讲清楚,先来聊一下 Java 的异常处理。

Figure 1: Java exception hierarchy

Figure 1: Java exception hierarchy

如上面的简易图所示, Java 的异常都是继承于 Throwable 这个分类的,而异常又是分 成不同的类型: Error, Exception; Exception 又分成 Checked ExceptionRuntimeException.

Error 一般都是了出现严重的问题,按照JDK 注释的说法,都是不应该 try-catch的:

An {() Error} is a subclass of {() Throwable} that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.

比如虚拟机挂了,或者JRE 出了问题就可能是 Error,前几天我就遇到一个JRE 的 Bug, 整个项目都挂 了:

Figure 2: JRE fatal error

Figure 2: JRE fatal error

我还顺便给 Oracle 报了个Bug :)

至于RuntimeException 就是类似数组越界,空指针这些异常,即无法在程序编译时发现,只有在运行的时候才会出 现的问题,所以叫做运行时异常(RuntimeException).

3 Checked Exception

Java的Checked Exception, 也就是Java 要求你必须在函数的类型里面声明或处理它可能抛出的异常。比如,你的函数如果是这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void foo(string filename) throws IOException
{
    File file = new File(filename);

    BufferedReader br = new BufferedReader(new FileReader(file));

    String st;
    while ((st = br.readLine()) != null)
	System.out.println(st);
}
}

Java 要求你必须在函数头部写上 throws IOException 或者是必须用 try-catch处理这个异常,因为readline() 的方法签名是:

1
2
String readLine(boolean ignoreLF) throws IOException {
}

所以编译器要求必须要处理这个异常,否则它就不能编译。

同理,在使用 foo()这个函数 的时候,可能会抛出 IOException 这个异常,由于编译器看到了这个声明,它会严格检 查你对 foo 函数的用法。

在我看来,CheckedException是Java 优良的设计之一,正因 为Checked Exception的存在,会更容易编写出正确处理错误的程序,更健壮的程序

4 Rust 的异常处理

Rust 是一个注重安全(Safety)的语言,而错误处理也是 Rust关注的要点之一。

Rust 主要是将错误划分成两种类型,分别是可恢复的错误(recoverable error) 和不可恢复错误 (unrecoverable error).

出现可恢复的错误的原因多种多样,例如打开文件的时候,文件找不到或者没有读权限等,开发者就应该对这种可能出现的错误进行处理;

而不可恢复的错误就可能是Bug 引起的,比如数组越界等。而其他常见的语言一般是没有没有区分 recoverable errorunrecoverable error的. 比如 Python, 用的就是 Exception.

而Rust 是没有 Exception, Rust 用 Result<T, E> 表示可恢复错误, 用 panic!() 来表示出现错误,并且中断程序的执行并退出(不可恢复错误)。

Result 是Rust 标准库的枚举:

1
2
3
4
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

TE都是泛型,T表示程序执行正常的时候的返回值,那E自然是程序出错时的返回 值。以标准库的打开文件的函数为例, std::io::Fileopen() 函数的签名如下:

1
2
3
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
    OpenOptions::new().read(true).open(path.as_ref())
}

忽略这个方法的参数,只看返回值类型:io::Result<File>, 又因为有 type Result<T> Result<T, Error>;=

这个 typedef 语句,所以返回值的完整版本时io::Result<File,io::Error>, 即调用 open 这个函数的时候,可能出现错误,出现错误时候返回一个 io::Error, 如果调用open没有问题的话,就会返回一个 File 的结构体,所以这个就类似 Java 的CheckedException,

只要声明了函数可能出现问题,在调用函数的时候就必须处理可能出现的错误,不然编译器就不会让你通过(Rust 的编译器就像位父亲那样对开发者耳提面命), 例如:

1
2
3
4
5
6
match File::open(&self.cache_path) {
    Ok(file) => println!("{:?}",file),
    Err(why) => {
	panic!("couldn't open {:?}", why.description())
    }
};

5 Java 的异常传递

在程序中,总会有一些错误需要处理,但是却不应该在错误出现的函数进行处理的情况(或者是,你很懒惰,只想应付一下编译器,不想处理出现的异常 :)

比如你正在编写一个类 库,里面有很多的IO 操作,有IO 操作的地方就有可能出现IOException. 如果出现异常, 你不应该自己在类库把异常给 try-catch了,如果这样,使用你类库的开发者就没办法知 道程序出现了异常,异常的堆栈也丢了。

比较合理的做法是,把IOException捕捉了,然后对 IOException 做一层包装,然后再抛给类库的调用者,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void doSomething() throws WrappingException{
    try{
	doSomethingThatCanThrowException();
    } catch (SomeException e){
	e.addContextInformation("there is something happen in doSomething() function, `Some Exception` is raised, balabala");
	//throw e;  //throw e, or wrap it  see next line.
	throw new WrappingException(e, more information about Some Exception, balabala);
    } finally {
	//clean up close open resources etc.
    }
}

当然,你也可以在添加了额外的信息之后,直接把原来的异常抛出来

6 Rust 的异常传递

刚刚谈了 Java 的异常传递,现在轮到 Rust 的异常传递了,既然Rust 没有 Exception一说,那 Rust 传递的自然也是 Result<T,E> 这个枚举类型(这里针对的是 可恢复错误,不可恢复错误出现错误的时候,会返回错误并弹出程序,自然不存在异常传递).

先来看看 Rust 的异常传递的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use std::io;
use std::io::Read;
use std::fs::File;

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

例子来自 Rust Book

先来看看函数的返回值 Result<String,io::Error>, 也就是说, read_username_from_file 正确执行的时候返回是 String, 错误的时候,返回的是 io::Error. 这里的异常传递是在出现 io::Error的时候,将错误原样返回,不然就是返 回函数执行成功的结果。

就异常传递的方式而言,Rust 和 Java 是大同小异:声明可能抛出的异常和成功时返回的结果,然后在遇到错误的时候,直接(或者包装一下)返回错误。

6.1 ? 关键字

虽说 Rust 的异常处理很清晰,但是每次都要 match 然后返回未免太繁琐了,所以 Rust 提供了一个语法糖来显示繁琐的异常传递:用 “?” 关键字进行异常传递:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::io;
use std::io::Read;
use std::fs::File;

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

同样的功能,但是模板代码却减少了很多 :)

6.2 unwrap 和 expect

虽说 Rust 的可恢复错误设计得很优雅,但是每次遇到可能出现错误得地方都要显示地进行 处理,不免让人觉得繁琐.

Rust 也考虑到这种情况了,提供了 unwrap()expect()让你舒心简单粗暴地处理错误:在函数调用成功的时候返回正确的结果,在 出现错误地时候直接 panic!(),并退出程序

6.2.1 unwrap

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

打开 hello.txt这个文件,能打开就返回文件 f,不能打开就 panic!() 然后退出程序。

1
2
3
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
/stable-dist-rustc/build/src/libcore/result.rs:868

6.2.2 expect

expect()unwrap()类似,只不过 expect()可以加上额外的信息:

1
2
3
4
5
use std::fs::File;

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

出现错误的时候,除了显示应有的错误信息之外,还会显示你自定义的错误信息:

1
2
3
thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
2, message: "No such file or directory" } }',
/stable-dist-rustc/build/src/libcore/result.rs:868

以上代码来自 Rust book

7 结语

以上只是浅谈了 Rust 的错误处理,以及和 Java 的异常处理机制的简单比较,接下来我会 谈谈如何自定义Error以及使用 erro_chain 这个库来优雅地进行错误处理 :)

如果想了解更多关于 Rust 异常处理的内容,可以查阅 Rust book Error handle

8 参考