系列文章目录

【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)

前言

Rust的可靠性源自于错误处理,大部分情况下,在编译时提示错误并处理。Rust 将错误分为两大类:不可恢复的和可恢复的错误。对于可恢复的错误,例如未找到文件,很能只向用户报告问题并重试操作。不可恢复的错误总是bug的症状,比如试图访问超过数组末尾的位置,因此我们希望立即停止程序。

大多数语言不区分这两种错误,并以相同的方式处理这两种错误,使用异常等机制。Rust 之中没有异常。

主要教材参考 《The Rust Programming Language》


一、不可恢复的错误(panic!)

1.1、不可恢复的错误

对于panic 的情况

  • 默认情况下,当 panic 发生,程序展开调用栈(工作量大),Rust 沿着调用栈往回走,清理每个遇到的函数中的数据
  • 立即终止调用栈,不进行清理,直接停止程序,内存需要 OS 进行清理

如果想让二进制文件更小,可以把设置从展开(unwinding)改为中止(Cargo.toml)

[profile.release]
panic = 'abort'

1.2、使用 panic

使用 panic!宏可以抛出一个 panic

fn main() {
    panic!("crash and burn!");
}

运行结果

 cargo run
   Compiling my-project v0.1.0 (~/Desktop/code/rust_code/my-project)
    Finished dev [unoptimized + debuginfo] target(s) in 0.86s
     Running `target/debug/my-project`
thread 'main' panicked at 'crash and burn!', src/main.rs:5:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

1.3、 索引越界错误

索引越界例子:

fn main() {
    let v = vec![1,2,3];
    v[99];
}

运行结果:

   Compiling my-project v0.1.0 (~/code/rust_code/my-project)
    Finished dev [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/my-project`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:6:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

从错误信息可以看出设置环境变量 RUST_BACKTRACE=1 就可以显示回溯信息;设置成 RUST_BACKTRACE=full
可以看到更多的详细信息。

二、可恢复的错误(Result<T,E>)

大部分错误其实并没有严重到无法恢复需要停止执行的地步,因此可以通过捕获 Result 枚举类型来从错误中恢复。

2.1、Result 枚举类型定义

enum Result<T, E> {  // T, E 都是泛型参数
    Ok(T),
    Err(E),
}
  • T:操作成功情况下,OK变体里面返回的数据的类型;
  • E:操作失败情况下,Err变体里面返回的数据的类型;

2.2、Result 枚举使用

2.2.1、处理 Result 的一种方式:match 表达式

use std::fs::File;

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

    let _f = match f {
        Ok(file) => file,
        Err(error) =>{
            panic!("Error open file {:?}",error)
        }
    };
}

2.2.2、匹配不同的错误

use std::fs::File;
use std::io::ErrorKind;

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

    let _f = match f {
        Ok(file) => file,
        Err(error) => match  error.kind() {
            ErrorKind::NotFound  => match File::create("hello.txt") {
                Ok(fc)=>fc,
                Err(e)=> panic!("文件创建失败:{:?}", e)
            }
            other_error => panic!("打开文件失败:{:?}", other_error),
        }
    };
}

2.2.3、使用 unwrap_or_else 优化上述代码

使用闭包可以让代码更加简洁,可以省去很多 match 的嵌套

use core::panic;
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt").unwrap_or_else(|error|{
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error|{
                panic!("文件创建失败:{:?}", error);
            })
        }else{
            panic!("打开文件失败:{:?}", error);
        }
    });
}

2.2.4、使用 unwrap 方法

match 表达式的一个快捷的写法

  • 如果 Result 结果是 OK,返回OK里面的值;
  • 如果 Result 结果是 Err,调用 panic! 宏;
   let f = File::open("hello.txt").unwrap();

2.2.5、使用 expect 方法

expect 和 unwrap 类似,unwrap不能指定 错误信息,而 expect 可以指定错误信息。

let f = File::open("hello.txt").expect("打开文件失败");

2.2.6、传播(propagating)错误

有些人喜欢原地处理,有些人需要将错误传递给上层调用处进行处理。 rust 提供?来进行错哦污传播哦。

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

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

let res = read_username_from_file();
dbg!(&res);
}

并不是所有的错误都能被隐式的被 Trait std::convert::From 上的 from 函数转换。用于针对不同错误原因,返回同一种错误类型:只要每个错误类型都实现了转换为返回的错误类型的from函数。

? 问号运算符只能用于返回值是 Result 或者Option,或者实现了 Try的类型。

2.2.7、main 函数返回值

默认的main 函数返回值是 () 类型,我们也可以让 main 函数返回 Result 类型。

use std::fs::File;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>>{
    let f = File::open("hello.txt")?;
    Ok(())
}

三、何时使用 panic!

3.1、总体原则

在定义一个可能失败的函数,优先考虑返回Result,否则返回panic。

3.2、使用panic!的场景

3.2.1、演示某些概念: unwrap

3.2.2、原型代码:unwrap、expect

3.2.3、测试:unwrap、expect

3.2.4、确定性的结果。

有时候你比编译器掌握更多的信息,你可以确定 Result 就是OK:unwrap

use std::net::IpAddr;

fn main() {
    let home: IpAddr= "127.0.0.1".parse().unwrap();

}

3.3、错误处理的指导性建议

当代码最终可能处于损坏状态(Bad State),最好用 panic!。

损坏状态(Bad State):某些假设、保证、约定或不可变性被打破。

  • 例如:非法的值、矛盾的值或空缺的值被传入代码
  • 以及下列的一条
    • 这种损坏状态不是预期能够偶尔发生的事情
    • 在此之前,您的代码如果处于这种损坏状态就无法运行;
    • 在您使用的类型没有一个好的方法来将这些信息(处于损坏状态)进行编码;

总结

以上就是今天要讲的内容

  • 讲解了panic 宏以及 panic的使用
  • 讲解了Result 枚举类型的使用
  • 讲解了 什么时候使用 panic
Logo

欢迎加入我们的广州开发者社区,与优秀的开发者共同成长!

更多推荐