• 17.错误处理
    • 17.1 Option和Result
      • Option
        • unwrap
        • map
        • unwrap_or
        • and_then
      • Result
        • unwrap
        • Result我们从例子开始
        • Result别名
      • 组合Option和Result
        • 复杂的例子
      • try!
        • 组合自定义错误类型
      • 总结

    17.错误处理

    错误处理是保证程序健壮性的前提,在编程语言中错误处理的方式大致分为两种:抛出异常(exceptions)和作为值返回。

    Rust 将错误作为值返回并且提供了原生的优雅的错误处理方案。

    熟练掌握错误处理是软件工程中非常重要的环节,让我一起来看看Rust展现给我们的错误处理艺术。

    17.1 Option和Result

    谨慎使用panic

    1. fn guess(n: i32) -> bool {
    2. if n < 1 || n > 10 {
    3. panic!("Invalid number: {}", n);
    4. }
    5. n == 5
    6. }
    7. fn main() {
    8. guess(11);
    9. }

    panic会导致当前线程结束,甚至是整个程序的结束,这往往是不被期望看到的结果。(编写示例或者简短代码的时候panic不失为一个好的建议)

    Option

    1. enum Option<T> {
    2. None,
    3. Some(T),
    4. }

    Option 是Rust的系统类型,用来表示值不存在的可能,这在编程中是一个好的实践,它强制Rust检测和处理值不存在的情况。例如:

    1. fn find(haystack: &str, needle: char) -> Option<usize> {
    2. for (offset, c) in haystack.char_indices() {
    3. if c == needle {
    4. return Some(offset);
    5. }
    6. }
    7. None
    8. }

    find在字符串haystack中查找needle字符,事实上结果会出现两种可能,有(Some(usize))或无(None)。

    1. fn main() {
    2. let file_name = "foobar.rs";
    3. match find(file_name, '.') {
    4. None => println!("No file extension found."),
    5. Some(i) => println!("File extension: {}", &file_name[i+1..]),
    6. }
    7. }

    Rust 使用模式匹配来处理返回值,调用者必须处理结果为None的情况。这往往是一个好的编程习惯,可以减少潜在的bug。Option 包含一些方法来简化模式匹配,毕竟过多的match会使代码变得臃肿,这也是滋生bug的原因之一。

    unwrap

    1. impl<T> Option<T> {
    2. fn unwrap(self) -> T {
    3. match self {
    4. Option::Some(val) => val,
    5. Option::None =>
    6. panic!("called `Option::unwrap()` on a `None` value"),
    7. }
    8. }
    9. }

    unwrap当遇到None值时会panic,如前面所说这不是一个好的工程实践。不过有些时候却非常有用:

    • 在例子和简单快速的编码中 有的时候你只是需要一个小例子或者一个简单的小程序,输入输出已经确定,你根本没必要花太多时间考虑错误处理,使用unwrap变得非常合适。
    • 当程序遇到了致命的bug,panic是最优选择

    map

    假如我们要在一个字符串中找到文件的扩展名,比如foo.rs中的rs, 我们可以这样:

    1. fn extension_explicit(file_name: &str) -> Option<&str> {
    2. match find(file_name, '.') {
    3. None => None,
    4. Some(i) => Some(&file_name[i+1..]),
    5. }
    6. }
    7. fn main() {
    8. match extension_explicit("foo.rs") {
    9. None => println!("no extension"),
    10. Some(ext) => assert_eq!(ext, "rs"),
    11. }
    12. }

    我们可以使用map简化:

    1. // map是标准库中的方法
    2. fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A {
    3. match option {
    4. None => None,
    5. Some(value) => Some(f(value)),
    6. }
    7. }
    8. // 使用map去掉match
    9. fn extension(file_name: &str) -> Option<&str> {
    10. find(file_name, '.').map(|i| &file_name[i+1..])
    11. }

    map如果有值Some(T)会执行f,反之直接返回None

    unwrap_or

    1. fn unwrap_or<T>(option: Option<T>, default: T) -> T {
    2. match option {
    3. None => default,
    4. Some(value) => value,
    5. }
    6. }

    unwrap_or提供了一个默认值default,当值为None时返回default

    1. fn main() {
    2. assert_eq!(extension("foo.rs").unwrap_or("rs"), "rs");
    3. assert_eq!(extension("foo").unwrap_or("rs"), "rs");
    4. }

    and_then

    1. fn and_then<F, T, A>(option: Option<T>, f: F) -> Option<A>
    2. where F: FnOnce(T) -> Option<A> {
    3. match option {
    4. None => None,
    5. Some(value) => f(value),
    6. }
    7. }

    看起来and_thenmap差不多,不过map只是把值为Some(t)重新映射了一遍,and_then则会返回另一个Option。如果我们在一个文件路径中找到它的扩展名,这时候就会变得尤为重要:

    1. use std::path::Path;
    2. fn file_name(file_path: &str) -> Option<&str> {
    3. let path = Path::new(file_path);
    4. path.file_name().to_str()
    5. }
    6. fn file_path_ext(file_path: &str) -> Option<&str> {
    7. file_name(file_path).and_then(extension)
    8. }

    Result

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

    ResultOption的更通用的版本,比起Option结果为None它解释了结果错误的原因,所以:

    1. type Option<T> = Result<T, ()>;

    这样的别名是一样的(()标示空元组,它既是()类型也可以是()值)

    unwrap

    1. impl<T, E: ::std::fmt::Debug> Result<T, E> {
    2. fn unwrap(self) -> T {
    3. match self {
    4. Result::Ok(val) => val,
    5. Result::Err(err) =>
    6. panic!("called `Result::unwrap()` on an `Err` value: {:?}", err),
    7. }
    8. }
    9. }

    没错和Option一样,事实上它们拥有很多类似的方法,不同的是,Result包括了错误的详细描述,这对于调试人员来说,这是友好的。

    Result我们从例子开始

    1. fn double_number(number_str: &str) -> i32 {
    2. 2 * number_str.parse::<i32>().unwrap()
    3. }
    4. fn main() {
    5. let n: i32 = double_number("10");
    6. assert_eq!(n, 20);
    7. }

    double_number从一个字符串中解析出一个i32的数字并*2main中调用看起来没什么问题,但是如果把"10"换成其他解析不了的字符串程序便会panic

    1. impl str {
    2. fn parse<F: FromStr>(&self) -> Result<F, F::Err>;
    3. }

    parse返回一个Result,但让我们也可以返回一个Option,毕竟一个字符串要么能解析成一个数字要么不能,但是Result给我们提供了更多的信息(要么是一个空字符串,一个无效的数位,太大或太小),这对于使用者是友好的。当你面对一个Option和Result之间的选择时。如果你可以提供详细的错误信息,那么大概你也应该提供。

    这里需要理解一下FromStr这个trait:

    1. pub trait FromStr {
    2. type Err;
    3. fn from_str(s: &str) -> Result<Self, Self::Err>;
    4. }
    5. impl FromStr for i32 {
    6. type Err = ParseIntError;
    7. fn from_str(src: &str) -> Result<i32, ParseIntError> {
    8. }
    9. }

    number_str.parse::<i32>()事实上调用的是i32FromStr实现。

    我们需要改写这个例子:

    1. use std::num::ParseIntError;
    2. fn double_number(number_str: &str) -> Result<i32, ParseIntError> {
    3. number_str.parse::<i32>().map(|n| 2 * n)
    4. }
    5. fn main() {
    6. match double_number("10") {
    7. Ok(n) => assert_eq!(n, 20),
    8. Err(err) => println!("Error: {:?}", err),
    9. }
    10. }

    不仅仅是mapResult同样包含了unwrap_orand_then。也有一些特有的针对错误类型的方法map_error_else

    Result别名

    Rust的标准库中会经常出现Result的别名,用来默认确认其中Ok(T)或者Err(E)的类型,这能减少重复编码。比如io::Result

    1. use std::num::ParseIntError;
    2. use std::result;
    3. type Result<T> = result::Result<T, ParseIntError>;
    4. fn double_number(number_str: &str) -> Result<i32> {
    5. unimplemented!();
    6. }

    组合Option和Result

    Option的方法ok_or

    1. fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> {
    2. match option {
    3. Some(val) => Ok(val),
    4. None => Err(err),
    5. }
    6. }

    可以在值为None的时候返回一个Result::Err(E),值为Some(T)的时候返回Ok(T),利用它我们可以组合OptionResult

    1. use std::env;
    2. fn double_arg(mut argv: env::Args) -> Result<i32, String> {
    3. argv.nth(1)
    4. .ok_or("Please give at least one argument".to_owned())
    5. .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
    6. .map(|n| 2 * n)
    7. }
    8. fn main() {
    9. match double_arg(env::args()) {
    10. Ok(n) => println!("{}", n),
    11. Err(err) => println!("Error: {}", err),
    12. }
    13. }

    double_arg将传入的命令行参数转化为数字并翻倍,ok_orOption类型转换成Resultmap_err当值为Err(E)时调用作为参数的函数处理错误

    复杂的例子

    1. use std::fs::File;
    2. use std::io::Read;
    3. use std::path::Path;
    4. fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    5. File::open(file_path)
    6. .map_err(|err| err.to_string())
    7. .and_then(|mut file| {
    8. let mut contents = String::new();
    9. file.read_to_string(&mut contents)
    10. .map_err(|err| err.to_string())
    11. .map(|_| contents)
    12. })
    13. .and_then(|contents| {
    14. contents.trim().parse::<i32>()
    15. .map_err(|err| err.to_string())
    16. })
    17. .map(|n| 2 * n)
    18. }
    19. fn main() {
    20. match file_double("foobar") {
    21. Ok(n) => println!("{}", n),
    22. Err(err) => println!("Error: {}", err),
    23. }
    24. }

    file_double从文件中读取内容并将其转化成i32类型再翻倍。
    这个例子看起来已经很复杂了,它使用了多个组合方法,我们可以使用传统的matchif let来改写它:

    1. use std::fs::File;
    2. use std::io::Read;
    3. use std::path::Path;
    4. fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    5. let mut file = match File::open(file_path) {
    6. Ok(file) => file,
    7. Err(err) => return Err(err.to_string()),
    8. };
    9. let mut contents = String::new();
    10. if let Err(err) = file.read_to_string(&mut contents) {
    11. return Err(err.to_string());
    12. }
    13. let n: i32 = match contents.trim().parse() {
    14. Ok(n) => n,
    15. Err(err) => return Err(err.to_string()),
    16. };
    17. Ok(2 * n)
    18. }
    19. fn main() {
    20. match file_double("foobar") {
    21. Ok(n) => println!("{}", n),
    22. Err(err) => println!("Error: {}", err),
    23. }
    24. }

    这两种方法个人认为都是可以的,依具体情况来取舍。

    try!

    1. macro_rules! try {
    2. ($e:expr) => (match $e {
    3. Ok(val) => val,
    4. Err(err) => return Err(::std::convert::From::from(err)),
    5. });
    6. }

    try!事实上就是match Result的封装,当遇到Err(E)时会提早返回,
    ::std::convert::From::from(err)可以将不同的错误类型返回成最终需要的错误类型,因为所有的错误都能通过From转化成Box<Error>,所以下面的代码是正确的:

    1. use std::error::Error;
    2. use std::fs::File;
    3. use std::io::Read;
    4. use std::path::Path;
    5. fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Box<Error>> {
    6. let mut file = try!(File::open(file_path));
    7. let mut contents = String::new();
    8. try!(file.read_to_string(&mut contents));
    9. let n = try!(contents.trim().parse::<i32>());
    10. Ok(2 * n)
    11. }

    组合自定义错误类型

    1. use std::fs::File;
    2. use std::io::{self, Read};
    3. use std::num;
    4. use std::io;
    5. use std::path::Path;
    6. // We derive `Debug` because all types should probably derive `Debug`.
    7. // This gives us a reasonable human readable description of `CliError` values.
    8. #[derive(Debug)]
    9. enum CliError {
    10. Io(io::Error),
    11. Parse(num::ParseIntError),
    12. }
    13. impl From<io::Error> for CliError {
    14. fn from(err: io::Error) -> CliError {
    15. CliError::Io(err)
    16. }
    17. }
    18. impl From<num::ParseIntError> for CliError {
    19. fn from(err: num::ParseIntError) -> CliError {
    20. CliError::Parse(err)
    21. }
    22. }
    23. fn file_double_verbose<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> {
    24. let mut file = try!(File::open(file_path).map_err(CliError::Io));
    25. let mut contents = String::new();
    26. try!(file.read_to_string(&mut contents).map_err(CliError::Io));
    27. let n: i32 = try!(contents.trim().parse().map_err(CliError::Parse));
    28. Ok(2 * n)
    29. }

    CliError分别为io::Errornum::ParseIntError实现了From这个trait,所有调用try!的时候这两种错误类型都能转化成CliError

    总结

    熟练使用OptionResult是编写 Rust 代码的关键,Rust 优雅的错误处理离不开值返回的错误形式,编写代码时提供给使用者详细的错误信息是值得推崇的。