【Rust笔记】05-错误处理

05 - 错误处理

  • Rust 有两种不同的错误处理机制:
    • 诧异(Panic
    • Result
  • 普通的错误使用 Result处理。
    • 由程序外部的事情导入:输入错误、网络中断,或者权限问题
    • 即时写的再完美的程序,也会不时地碰到这样的错误。
  • 诧异用于处理 “永远不该发生的错误”,比如程序本身的 Bug。

5.1-Panic

  • 以下常见的程序 Bug,都会产生诧异:
    • 越界访问数组
    • 整数被零除
    • 在值为 NoneOption 上调用.unwrap()
    • 断言失败
  • Rust 有一个同名的宏 Panic!(),用于在代码中主动触发诧异。接收可选的 println!() 风格的参数,用于构建错误消息。
  • 针对诧异的规避措施:
    • 在诧异发生时展开(unwind)栈,是默认选项;
    • 中止程序。

5.1.1 - 展开栈

  • 当前函数调用被清理后,控制流交还至调用者,再以同样的方式清除其变量和参数。接下来是那个函数的调用者,总之沿栈逐层清理。
  • 当前函数石油的任何临时值、局部变量或参数都会按照它们创建的顺序被反向清除。
  • 最后,线程退出。
    • 如果发生诧异的线程是主线程,那么整个程序都会退出,退出码为非 0.
  • 诧异是安全的,其行为是明确定义的。如 Java 语言中的 RuntimeException 或 C++ 语言中的 std::logic_error
  • 诧异是线程级别的:一个线程诧异时,其他线程可以正常运行自己的业务逻辑
  • 标准库函数 std::panic::catch_unwind,可以捕获栈展开,允许诧异线程存活并继续进行。
    • Rust 的测试套件在断言失败时,使用了这个机制来恢复线程执行。
    • 可以使用线程和 catch_unwind() 来处理诧异。
    • 这种机制只能捕获展开栈的诧异,而并非所有诧异都会展开栈(另一些会中止进程)。

5.1.2 - 中止进程

在以下两种情况,程序发生 Bug 后会中止进程:

  • 在 Rust 展开第一个函数后的清理期间,

    .drop()
    

    方法触发了第二个诧异:

    • 这种诧异会被认为是间接致使的,Rust 会停止展开栈并中止整个进程。
  • Rust 的诧异行为是自定义的情况:

    • 编译时加上 -C panic=abort,那么编译后程序中的第一个诧异就会立即中止进程。
    • 这个参数设置,使得 Rust 编译器无须知道如何展开栈,因此可以减少编译后代码的大小。

5.2-Result

  • Rust 没有异常,但是相反,当函数执行失败时,可以通过一个返回的 Result 类型来表示。

    fn get_weather(location: LatLng) -> Result<WeatherReport, io::Error>
    
    • 上述函数执行成功,会返回 Ok(weather),此时 weather 的值是一个新的 WeatherReport 值。
    • 执行失败,会返回 Err(error_value),此时 error_value 就是一个解释除了什么错误的 io::Error 值。

5.2.1 - 捕获错误

  • 处理 Result 的一种方式,是使用 match 表达式。

    // 相当于Python中的try机制
    match get_weather(hometown) {
        ok(report) => {
            display_weather(hometown, &report);
        }
        Err(err) => {
            println!("error querying the weather: {}", err);
            schedule_weather_retry();
        }
    }
    
  • 处理 Result 的另一种方式,是使用 Result<T, E> 提供的几种方法,每种方法都有一个形如上述的 match 表达式。如下所示常用的方法有:

    • result.is_ok()result.is_err() 返回 bool 值:告知 result 是成功的结果还是错误的结果。

    • result.ok() 返回 Option<T> 类型的成功值:成功返回 Some(success_value);失败返回 None,并丢弃错误值。

    • result.err() 返回 Option<E> 类型的错误值。

    • result.unwrap_or(fallback) 返回 T 类型的成功值。如果失败返回一个后备值 fallback,并丢弃错误值。

      // 设定一个后备值,用来预测某地天气
      const THE_UAL: WeatherReport = WeatherReport::Sunny(72);
      
      // 如果预测成功,返回实时的天气情况
      // 如果预测失败,则以常规的值为后备
      let report = get_weather(los_angeles).unwrap_or(THE_UAL);
      display_weather(los_angeles, &report);
      
    • result.unwrap_or_else(fallback_fn) 返回 T 类型的成功值。但是传入的参数是一个函数或闭包。只有在返回错误结果时,才会调用 fallback_fn

      let report = 
          get_weather(hometown)
          .unwrap_or_else(|_err| vague_prediction(hometown));
      
    • result.unwrap() 会返回 T 类型的成功值。如果失败,会导致诧异。

    • result.expect(message)result.unwrap() 相同,不过需要提供诧异时打印到控制台的消息 message

  • 借用 Result 中值的引用的方法,Result 的原数据结果不会损坏:

    • result.as_ref()Result<T, E> 转换为 Result<&T, &E>,即借用现有 result 中成功或错误的引用。
    • result.as_mut(),借用的是可修改引用。返回的类型为 Result<&mut T, &mut E>

5.2.2-Result 类型别名

  • 省略 Result 的错误类型,即为 Result 类型别名:

    fn remove_file(path: &Path) -> Reust<() >
    
    • 避免模块中的每个函数将一个错误类型重复写很多遍。
    pub type Result<T> = result::Result<T, Error>;
    
    • 上述代码定义了一个公有类型 std::io::Result<T>,以硬编码的 std::io::Error 作为错误类型的 Result<T, E> 的别名。实际使用中,如果 use std::io,那么 Rust 就会认为 io::Reust<String>Result<String, io::Error> 的简写

5.2.3 - 打印错误

  • 将错误转存到终端,然后继续执行:

    println!("error querying the weather: {}", err);
    
  • std:io::Errorstd::fmt::Errorstd::str::Utf8Error 等错误类型有相同的公共接口:std::error::Error 特型,它们都支持以下特点:

    • 都可以使用 println!() 来打印。打印错误时以 {} 作为格式描述符,可以显示简略的错误消息;也可以使用 {:?} 为格式描述符,这样可以打印 debug 版的错误消息。
    • 支持 err.description() 方法,返回 &str 类型的错误消息。
    • 支持 err.cause() 方法,返回一个 Option<&Error> 类型,这是触发 err 的底层错误。即错误的真正原因。
  • 打印错误消息不一定会打印出错误的原因。如果需要打印所有可用信息,可使用如下函数:

    use std::error::Error;
    use std::io::{Write, stderr};
    
    /// 把错误消息转存到stderr
    ///
    /// 如果在构建当前错误消息,或写入stderr时,发生了另一个错误,则忽略该错误。
    fn print_error(mut err: &Error) {
        let _ = writeln!(stderr(), "error: {}", err);
        while let Some(cause) = err.cause() {
            let _ = writeln!(stderr(), "caused by: {}", cause);
            err = cause;
        } 
    }
    
  • 标准库的错误类型不包含栈追踪信息,但是使用 error-chain 包可以方便地定义自己的错误类型,以支持在创建时获取栈追踪信息。这个包使用了 backtrace 捕获栈信息。

5.2.4 - 传播错误

  • 含义:为了避免代码冗余,可以不立即捕获和处理错误,让错误可以沿调用栈向上传播
    • ? 操作符
    • try!()
  • ? 操作符:可以在任何产生 Result 的表达式后面添加 ?,如:
let weather = get_weather(hometown)?;
  • 如果返回了成功结果,那么 ? 操作符会打开 Result 并取出其中的成功值。

  • 如果返回了错误结果,那么 ? 操作符会立即从闭合函数中返回,将错误结果沿调用链向上传播。

  • 只能对返回值为 Result 的函数使用 ?

  • 上述代码等价于:

    let weather = match get_weather(hometown) {
        ok(success_value) => success_value;
        Err(err) => return Err(err)
    };
    
  • try!() 宏:扩展为一个类似上述 match 的表达式。

    let weather = try!(get_weather(hometown));
    
  • ? 操作符的用法举例:

    use std::fs;
    use std::io;
    use std::path::Path;
    
    fn move_all(src: &Path, dst: &Path) -> io::Result<()> {
        for entry_result in src.read_dir()? {             // 打开dir可能失败
            let entry = entry_result?;                    // 读取dir可能失败
            let dst_file = dst.join(entry.file_name());
            fs::rename(entry.path(), dst_file)?;          // 重命名可能失败
        }
        ok(())   // 如果执行成功,则返回成功值
    }
    

5.2.5 - 处理多种错误类型

  • 从文件中读取一行内容,并解析为整数时,会产生两种不同的潜在错误类型:

    use std::io::{self, BufRead};
    
    /// 从文本文件中读取整数
    /// 这个文件中的每一行应该都有一个数值
    fn read_numbers(file: &mut BufRead) -> Result<Vec<i64>, io::Error> {
        let mut numbers = vec![];
        for line_result in file.lines() {
            let line = line_result?;     // 读取行的内容可能失败
            numbers.push(line.parse?);   // 解析整数有可能失败
        }
        Ok(numbers)
    }
    
    • line_result 的错误类型是 Result<String, std::io::Error>

    • line.parse() 的错误类型是 Result<i64, std::num::ParseIntError>;在编译时,Rust 会常识把 ParseIntError 转换为 io::Error,但是这种转换不存在,就会得到如下的类型错误:

      the trait 'std::convert::From<std::num::ParseIntError>' is not implemented for 'std::io::Error'
      
  • 处理上述错误的方法:

    • 采用 error-chain 包;

    • 采用内置特性:所有标准库的错误类型都可以转换为 Box<std::error::Error> 类型,因此可以定义如下类型别名:

      type GenError = Box<std::error::Error>;
      type GenResult<T> = Result<T, GenError>;
      // 然后,可以把read_numbers()的返回类型改为GenResult<Veci64>
      // ?操作符,会根据需求,自动将任意错误类型转换为GenError。
      
    • 把任意错误转换为 GenError 类型,也可以调用 GenError::from() 方法。

      let io_error = io::Error::new(          // 创建一个自定义的io::Error
          io::ErrorKind::Other, "timed out"
      );
      return Err(GenError::from(io_error));   // 手工转换为GenError
      
    • 使用 GenError 的缺点:返回类型不再精确地传达调用者可以预测的错误类型。如果调用的函数返回 GenResult,但只想处理一种特定的错误,让其他所有错误传播出去,那么可以使用泛型方法 error.downcast_ref::<ErrorType>()。如果恰好是想要的那个错误类型,那么该方法会借用这个错误的引用:

      loop {
          match compile_project() {
              Ok(()) => return Ok(()),
              Err(err) => {
                  if let Some(mse) = err.downcast_ref::<MissingSemicolonError>() {
                      insert_semicolon_in_source_code(mse.file(), mse.line())?;
                      continue;
                  }
                  return Err(err);
              }
          }
      }
      

5.2.6 - 处理不会发生的错误

  • 把数字字符串转换成实际的数值:

    let num = str.parse::<u64>();
    
    let num = str.parse::<u64>().unwrap();
    // 处理当字符串不是数字时的编写错误,用诧异输出
    // 但是如果数字过长,而超过了u64,那么此时会产生一个bug
    
  • 如果错误代表的是一个非常严格或奇怪的条件,希望在出错时差异,那么可以采用如下的.elapsed() 方法:

    fn print_file_age(filename: &Path, last_modified: SystemTime) {
        let age = last_modified.elapsed().expect("System clock drift");
        ...
    }
    // .elapsed()方法,只有当系统时间早于文件创建时间时才会失败
    // 此时会出发诧异,而不是处理错误或将错误传播给调用者。
    

5.2.7 - 忽略错误

  • let _ = ...:用来禁止某些错误

    • 把 stderr 通过管道发送到另一个进程,但是该进程被杀死了,会触发难以避免的错误。可以通过以下方法忽略这个错误:
    let _ = writeln!(stderr(), "error: {}", err);
    

5.2.8 - 在 main 中处理错误

  • main() 函数不能使用 ? 操作符

  • .expect() 方法:在 main() 函数中处理错误的简单方式

    fn main() {
        calculate_tides().expect("error");
    }
    // 主线程中的诧异会打印错误消息,然后以一个非零退出码退出。
    // 在较小的程序中比较推荐使用。
    
  • 优化上述代码:采用 if let 表达式,只在调用 calculate_tides() 返回错误时打印错误消息。

    use std::error::Error;
    use std::io::{Write, stderr};
    
    /// 把错误消息转存到stderr
    ///
    /// 如果在构建当前错误消息,或写入stderr时,发生了另一个错误,则忽略该错误。
    fn print_error(mut err: &Error) {
        let _ = writeln!(stderr(), "error: {}", err);
        while let Some(cause) = err.cause() {
            let _ = writeln!(stderr(), "caused by: {}", cause);
            err = cause;
        } 
    }
    
    fn main() {
        if let Err(err) = calculate_tides() {
            print_error(&err);
            std::process::exit(1);
        }
    }
    

5.2.9 - 声明自定义错误类型

use std;
use std::fmt;

// 编写一个JSON解释器,并让它有自己的错误类型
// json/src/error.rs,可以通过json::error::JsonError调用
#[derive(Debug, Clone)]
pub struct JsonError {
    pub message: String,
    pub line: usize,
    pub column: usize,
}

/// 方法一:简单创建
return Err(JsonError {
    message: "expected ']' at end of array".to_string(),
    line: current_line,
    column: current_column
});

/// 方法二:接近标准错误类型
// 错误可以打印出来
impl fmt::Display for JsonError {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        write!(f, "{} ({}:{})", self.message, self.line, self, self.column)
    }
}

// 错误实现std::error::Error特型
impl std::error::Error for JsonError {
    fn description(&self) -> &str {
        &self.message
    }
}

详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第七章
链接地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

phial03

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值