Rust的错误处理(一)

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

Updated on 2018-02-07 18:42 (Created on: 2018-02-05 20:35)

前言

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

Java 的异常处理

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

                   +-----------+
				   | Throwable |
                   +-----------+
                    /         \
		   /           \
          +-------+          +-----------+
          | Error |          | Exception |
          +-------+          +-----------+
		  /  |  \           / |        \
	    \________/    \______/       	 \
			                     +------------------+
	unchecked	      checked    | RuntimeException |
	                             +------------------+
							      /   |    |      \
						         \_________________/
							         unchecked

如上面的简易图所示, Java 的异常都是继承于 Throwable 这个分类的,而异常又是分 成不同的类型: Error, Exception; Exception 又分成 Checked ExceptionRuntimeException. Error 一般都是了出现严重的问题,按照JDK 注释的说法,都是不 应该 try-catch的:

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

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

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

Checked Exception

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

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() 的方法签名是:

String readLine(boolean ignoreLF) throws IOException {
}

所以编译器要求必须要处理这个异常,否则它就不能编译。同理,在使用 foo()这个函数 的时候,可能会抛出 IOException 这个异常,由于编译器看到了这个声明,它会严格检 查你对 foo 函数的用法。在我看来,CheckedException是Java 优良的设计之一,正因 为Checked Exception的存在,会更容易编写出正确处理错误的程序,更健壮的程序

Rust 的异常处理

Rust 是一个注重安全(Safety)的语言,而错误处理也是 Rust 关注的要点之一。Rust 主 要是将错误划分成两种类型,分别是可恢复的错误(recoverable error) 和不可恢复错误 (unrecoverable error). 出现可恢复的错误的原因多种多样,例如打开文件的时候,文件 找不到或者没有读权限等,开发者就应该对这种可能出现的错误进行处理;而不可恢复的错 误就可能是Bug 引起的,比如数组越界等。而其他常见的语言一般是没有没有区分 recoverable errorunrecoverable error的. 比如 Python, 用的就是 Exception. 而Rust 是没有 Exception, Rust 用 Result<T, E> 表示可恢复错误, 用 panic!() 来表示出现错误,并且中断程序的执行并退出(不可恢复错误)。Result 是Rust 标准库的枚举:

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

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

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 的编译器就像位父亲那样对开发者耳提面命), 例如:

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

Java 的异常传递

在程序中,总会有一些错误需要处理,但是却不应该在错误出现的函数进行处理的情况(或 者是,你很懒惰,只想应付一下编译器,不想处理出现的异常 :),比如你正在编写一个类 库,里面有很多的IO 操作,有IO 操作的地方就有可能出现IOException. 如果出现异常, 你不应该自己在类库把异常给 try-catch了,如果这样,使用你类库的开发者就没办法知 道程序出现了异常,异常的堆栈也丢了。比较合理的做法是,把IOException捕捉了,然 后对 IOException 做一层包装,然后再抛给类库的调用者,例如:

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

}

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

Rust 的异常传递

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

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 是大同小异:声明可能抛 出的异常和成功时返回的结果,然后在遇到错误的时候,直接(或者包装一下)返回错误。

? 关键字

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

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

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

unwrap 和 expect

虽说 Rust 的可恢复错误设计得很优雅,但是每次遇到可能出现错误得地方都要显示地进行 处理,不免让人觉得繁琐,Rust 也考虑到这种情况了,提供了 unwrap()expect()让你舒心简单粗暴地处理错误:在函数调用成功的时候返回正确的结果,在 出现错误地时候直接 panic!(),并退出程序

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

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

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
expect

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

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" } }',
/stable-dist-rustc/build/src/libcore/result.rs:868

以上代码来自 Rust book

结语

以上只是浅谈了 Rust 的错误处理,以及和 Java 的异常处理机制的简单比较,接下来我会 谈谈如何自定义Error以及使用 erro_chain 这个库来优雅地进行错误处理 :) 如果想 了解更多关于 Rust 异常处理的内容,可以查阅 Rust book Error handle

参考