3

Rust 语言学习笔记(四)

 8 months ago
source link: https://yanbin.blog/rust-language-learning-4/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Rust 语言学习笔记(四)

2024-01-07 | 阅读(4)

试手了一下 Rust, 发现止今所学知识尚浅,不少情况处理起来很是茫然,比如经常得到的值是 Result 或 Option, 有时候要 .unwrap(), 有时得后面加个问号,或 .await。还有从自定义函数里返回一个引用都不容易,到处是 move, borrow, owner, lifetime 之类的错误信息。所以说,快充五分钟还真不敢上路,继续学习 Rust 的 Result, 说起它又必须与 Rust 的错误处理联系起来。

编程语言在处理错误无外乎就两种哲学

  1. 得到正确的结果或触发异常,异常不就地处理则向外传播
  2. 总是能得到一个结果,于结果中获知是否有异常,如 Option<T>, Result<T, E>

其实本人更钟爱前一种方式,因为正常逻辑可以更流畅的表达,不至于被众多的 if 语句所打断; 而异常的话可以就地解决,或者延迟到某处去集中处理。Java 多是用第一种方式,所以某些框架尽量使用 Unchecked 异常,不强制插入 try/catch 块。

第二种方式,在 shell 或 C++ 很常见,比如 shell 下每调用一个命令都有返回数值, 即 $?, 0 为正常,非 0 为有错误; C++ 的 GetLastError() 也是一样的意思。

现在所学的 Rust 在处理结果和错误时,采用了第二种方式,不过使用上 Rust 的模式匹配处理起 Option<T>, Result<T, E> 倒也不难,而且 Option<T> 和 Result<T, E> 这样的返回结果还用来向上传播异常,出错时还能追踪到异常栈。

Rust 提供了分层式错误处理的几种方案

  1. Option<T>: 有无值
  2. Result<T, E>: 有无错误
  3. Panic: 错误不可恢复,Go 语言也是用这个 panic 关键字
  4. Abort: 灾难性错误,一般都要终止进程了

实际中建议用 Result<T, E> 和 Panic 来分别处理可恢复和不可恢复的错误

Result<T, E> 是 Rust 内置并被自动引入的枚举

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

下面自定义一个方法返回 Result<T, E>, 然后看几个典型的应用

fn add_one(x: i32) -> Result<i32, &'static str> {
    if x == i32::MAX {
        Err("overflow")
    } else {
        Ok(x + 1)
fn main() {
    let v = add_one(32).unwrap();
    println!("v: {}", v);  // v: 33
    let e = add_one(i32::MAX).unwrap_err();
    println!("e: {}", e);  // e: overflow
    if let Ok(ok_value) = add_one(32) {
        println!("ok_value: {}", ok_value)  // ok_value: 33
    if let Err(err_str) = add_one(i32::MAX) {
        println!("err_str: {}", err_str)   // err_str: overflow

如果对 Err("overflow") 进行 .unwrap() 操作,会得到错误(Panic)

called Result::unwrap() on an Err value: "overflow"

并在 Debug 模式下输出整个异常栈

当然对 Ok(33) 也不能  unwrap_err() 操作, 也会有 Panic

called Result::unwrap_err() on an Ok value: 33

但可以用 Result<T, E> 的其他方法

  1. unwrap_or(default): 有错误则得到一个默认值
  2. expect("custom error"): 有错误仍然是一个 Panic, 可进一步自定义错误信息,如 Err("overflow").expect("custom error") 会看到  custom error: "overflow"。有一点像 PHP foo() or die("死给你看") 的意思
  3. expect_err(msg): 用到 Err 身上的方法,如果不是错误可不成
  4. is_ok(), is_error() 的判断
  5. ok(), error() 分别针对 Ok, Err 得到相应的 Option<T> , Option<E> 结果
  6. 其他更多如 unwrap_or_else(op), unwrap_or_default(default) 等

?unwrap()

某些时候 ?unwrap() 的快捷操作,但不是任何地方都适用。我们先在 main() 函中试下

fn main() {
    let x = add_one(32)?; // 类型自动推断 x: i32
    println!("{}", x);

在 Result<T, E> 后加个 ? 号确实能让 x 类型在 IntelliJ IDEA 推断为 i32, 但无法通过编译,错误是

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
  --> src/main.rs:12:24
9  | fn main() {
   | --------- this function should return `Result` or `Option` to accept `?`
12 |     let x = add_one(32)?;
   |                        ^ cannot use the `?` operator in a function that returns `()`
   = help: the trait `FromResidual<Result<Infallible, &str>>` is not implemented for `()`

就因为 main() 不是返回 Result 或 Option。? 的操作是用来无法完成 unwrap() 时继续向外传播相应的 Result 或 Option, 所以创建一个返回 Result<T, E> 的函数来用 ? 就没问题

fn foo(x: i32) -> Result<i32, &'static str> {
    let v: i32 = add_one(x)?;     // 正常
    Ok(v / 2)
fn main() {
    let x: i32 = foo(i32::MAX).unwrap();
    println!("{}", x);

因为 foo 函数返回的是 Result<T, E> 所以其中的 add_one(x)? 不再抱怨了。

再仔细看上面错误,好像调用函数返回 Result 或 Option 就能在其中用 ? 代替 unwrap(), 那把 foo 函数改成返回 Option

fn foo(x: i32) -> Option<i32> {
    let v: i32 = add_one(x)?;
    Some(v / 2)

编译又不过

error[E0277]: the `?` operator can only be used on `Option`s, not `Result`s, in a function that returns `Option`
  --> src/main.rs:10:28
9  | fn foo(x: i32) -> Option<i32> {
   | ----------------------------- this function returns an `Option`
10 |     let v: i32 = add_one(x)?;
   |                            ^ use `.ok()?` if you want to discard the `Result<Infallible, &str>` error information

所以前面说的 Result or Option 是骗人的,准确的说应该是函数中用 ? 代替 .unwrap() 的话,必须保证当前函数也是返回一样的类型,Result 对 Result, Option 对  Option。 因为 ? 代替 .unwrap() 的语义是,如果能 unwrap() 则往下处理,否则把 Result::Err 或 Option:None 的值作当前函数的返回值,也就要求 Err 中的错误类型要匹配。具体来说是

Option

fn foo(x: i32) -> Option<f64> {
    let v: i32 = bar(x)?;   // 用 bar(x)? 的话,foo 方法返回任意类型的 Option 都行
fn bar(x: i32) -> Option<i32> {

Result<T, E>

fn foo(x: i32) -> Result<f64, &'static str> {
    let v: i32 = bar(x)?;    // 用 bar(x)? 的话,foo 方法只需保证返回的 Result 的 错误部类类型一致,此处都必须是 &'static str
fn bar(x: i32) -> Result<i32, &'static str> {

ChatGPT 说推荐用 ?, 这样能更优雅的向上传播异常,除非确定 Result/Option 中有值就用 unwrap()。测试了一下,确实,在 foo() 函数中用了  add_on(x)? 异常栈里就能看到  foo() 函数调用,用 add_on(x).unwrap() 则差一些。

如果把 ? 号串起来,写成

let x = foo()?.bar()?.qux()?;

有种似曾相识的感觉,对就是 Groovy 的 Safe Navigation operator。

前面正好又从 Result<T, E> 没到了 Option<T>, 那就看看 Option<T> 应如何处理

Option<T> 也是一个枚举

enum Option<T> {
    None,
    Some(T),

包括两个值,有还无。Option 也有类似的 unwrap(), or(), or_else(), 通过 ok_or() 和 ok_or_else() 方法又可以把 Option 转换成一个 Result<T, E> 类型。

还有就是把  Option 用 match 或 if let 的模式匹配, 如 if let Some(x) = foo() { ...process x... }

抛出和捕获异常

相信我们多数人比较习惯的异常的处理方式是 throw/try/catch/finally, Python 用的关键字是 raise/try/except/finally, Go 的关键字是 defer/panic/recover。回到这里的 Rust 用的是什么语法呢,panic/catch_unwind, Rust 没有传统语言的 finally

use std::panic::catch_unwind;
fn foo(x: i32) -> i32 {
    if x == 0 {
        panic!("can't process 0")
fn main() {
    let y = catch_unwind(|| {  // y 的类型自动推断为 std::thread::Result<i32>
        foo(0)       // 如果在这个闭包体中返回不同的类型,y 也将被推断为相应的 std::thread::Result<X> 类型
    println!("{:?}", y);  // Err(Any { .. })

std::thread::Result 是一个自定义的 Result

pub type Result<T> = crate::result::Result<T, Box<dyn Any + Send + 'static>>;

通过 catch_unwind() 又重新把 panic 转换成的 Result, 还是要用 unwrap() 之类的方法去判断成功或失败,或获得其中的值

程序虽然能正常恢复,但在控制台下执行的是 cargo build --release 生成的执行文件,仍然打印出信息

thread 'main' panicked at src/main.rs:13:9:
can't process 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Err(Any { .. })

如果 foo(0) 换成 foo(10), 执行后控制台输出为

Ok(20)

catch_unwind() 的最终目的是把 panic 转换成 Result。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK