5分钟速读之Rust权威指南(十六)

205 阅读4分钟

可恢复错误与Result

一些没有严重到影响程序退出的错误属于可恢复错误,比如因为文件不存在导致打开文件失败时,应该考虑创建文件而不是终止进程。

在rust中使用内置枚举Result来处理这种错误:

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

例如打开一个存在的文件:

// 引入文件操作模块
use std::fs::File;

// 打开项目中的配置文件
let result = File::open("./Cargo.toml");

// 匹配打开的结果
let file = match result {
  // 与Option一样,Ok和Err可以直接使用
  // 如果是OK类型,将f绑定给file
  Ok(f) => f,
  // 如果是Err类型,则主动触发报错
  Err(err) => panic!("打开文件失败:{:?}", err)
};

println!("{:?}", file)
// File { fd: 3, path: "/Cargo.toml", read: true, write: false }

当文件不存在时:

// 打开一个不存在的文件
let result = File::open("./Cargo2.toml");
let file = match result {
  Ok(f) => f,
  Err(err) => panic!("打开文件失败:{:?}", err)
};

println!("{:?}", file)
// thread 'main' panicked at '打开文件失败:Os { code: 2, kind: NotFound, message: "No such file or directory" }'

匹配不同错误

根据错误类型进行处理,打开文件不存在时,我们则创建一个,如果是因为没有权限,则再抛出错误:

use std::fs::File;

// 引如错误类型枚举
use std::io::ErrorKind;

let result = File::open("./Cargo2.toml");
let file = match result {
	Result::Ok(file) => file,
	// 匹配错误类型
	Result::Err(err) => match err.kind() {
    // 如果文件未找到,那么创建文件
		ErrorKind::NotFound => match File::create("./Cargo2.toml") {
			Ok(fc) => fc,
			Err(e) => panic!("创建文件失败:{:?}", e),
		},
    // 其他错误类型则不处理
		_ => panic!("打开文件失败:{:?}", err),
	},
};
println!("{:?}", file)

利用闭包简化嵌套match表达式:

// 使用map_err方法,当result是Err时会执行闭包中的代码
let file = File::open("./Cargo2.toml").map_err(|error| {
  if error.kind() == ErrorKind::NotFound {
    // unwrap_or_else用于当result是Err时通过闭包计算默认值
    File::create("./Cargo2.toml").unwrap_or_else(|error| {
      panic!("创建文件失败:{:?}", error)
    })
  } else {
    panic!("打开文件失败:{:?}", error)
  }
})
println!("{:?}", file)

unwrap和expect

当Result的返回值是Ok变体时,unwrap就会返回Ok内部的值。而当Result的返回值是Err变体时,unwrap则会替我们调用panic!:

// 遇到错误直接抛出
let file = File::open("./Cargo3.toml").unwrap();
// expect可以自定义错误信息
let file = File::open("./Cargo3.toml").expect("出错了");

传播错误

使用传播错误的可以让调用者来控制如何处理错误:

use std::fs::File;
use std::io::{ self, Read };

fn read_username_from_file() -> Result<String, io::Error> {
  let mut file = match File::open("./Cargo.toml") {
    Result::Ok(file) => file,
    // 如果读取失败则把错误信息返回,交给调用者处理
    Result::Err(e) => return Result::Err(e),
  };
  let mut str = String::new();
  match file.read_to_string(&mut str) {
    Result::Ok(_) => Result::Ok(str),
    // 这里也同样交给调用者处理,由于match是最后一个语句,所以这里不用加return
    Result::Err(e) => Result::Err(e),
  }
}

传播错误的模式在Rust编程中经常使用,所以rust专门提供了一个问号运算符(?)来简化它的语法:

fn read_username_from_file() -> Result<String, io::Error> {
  let mut file = File::open("./Cargo.toml")?;
  let mut str = String::new();
  file.read_to_string(&mut str)?;
  Result::Ok(str)
}
// 问号意味着如果处理正常,则当前表达式返回处理结果,
// 如果报错,则将表达式所在的函数结束执行并返回Result::Err错误信息。

问号还支持链式调用,这个语法在JS中也有哦:

fn read_username_from_file() -> Result<String, io::Error> {
  let mut str = String::new();
  File::open("./Cargo.toml")?.read_to_string(&mut str)?;
  Result::Ok(str)
}

从文件中读取字符串是一种常见的操作,所以rust提供了一个函数fs::read_to_string,用于打开文件,创建一个新String,并将文件中的内容读入这个String,接着返回给函数调用者:

use std::fs;
fn read_username_from_file() -> Result<String, io::Error> {
  fs::read_to_string("./Cargo2.toml")
}

问号有一个限制,只能用于返回Result类型的函数,由于?是通过match处理Result的语法糖,当处理的是Result::Err类型时,由于?会把错误信息返回,这就要求函数的返回值必须是Result::Err类型:

  // 在main函数中使用问号将报错,因为main函数的返回值不是Result

  | | fn main() {
  | |     File::open("./Cargo2.toml")?
  | |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
  | | }
  | |_- this function should return `Result` or `Option` to accept `?`
  |
  = help: the trait `Try` is not implemented for `()`
  = note: required by `from_error`

不过有一种办法可以在main函数中使用问号:

// 这里的Box<dyn Error>被称作trait对象,将在后边介绍
fn main() -> Result<(), Box<dyn Error>>{
  File::open("./Cargo2.toml")?;
  Result::Ok(())
}