Rust 语法梳理与总结(下)

楔子

最后我们来复习一下 Rust 的模块与错误处理,等把这两部分说完之后,我们就可以继续学习后面的内容了。

先来看看模块。

模块

Rust 提供了一套强大的模块(module)系统,可以将代码按层次划分成多个逻辑单元(模块),并管理这些模块内部条目的可见性。所以模块就是条目的集合,而条目可以是:函数、结构体、trait、impl 块、变量、常量等等,甚至也可以是其它模块。

我们可以使用 mod 关键字定义一个模块,默认情况下,模块内的条目都是私有的,除非我们使用 pub 关键字将其变成公有。但是结构体特殊,结构体开头加 pub 只表示结构体是公有的,但字段还是私有的,如果希望字段也公有,那么还要在每个字段前面加上 pub。

// 定义一个名为 a 的模块
mod a {
    fn func1() {
        println!("我是私有函数 func1");
    }
    pub fn func2() {
        println!("我是公有函数 func2");
    }
    pub fn func3() {
        println!("我是公有函数 func3");
        // 私有的条目对外是不可见的,但是对内可见
        func1();
    }
}
fn main() {
    // 通过模块 a 只能找到 func2 和 func3
    // func1 是私有的,外界无法获取
    a::func2();
    /*
    我是公有函数 func2
    */
    a::func3();
    /*
    我是公有函数 func3
    我是私有函数 func1
    */
}

通过模块名可以调用模块内部的条目,访问方式是通过 ::。但是要注意条目的可见性,默认都是私有的,如果想通过模块名访问,那么一定要在条目的前面加上 pub 关键字。那么可能有人好奇了,为啥 mod a 的前面没有 pub 呢?原因是模块 a 位于最外层,和调用方处于同一级,因此不需要 pub。

另外在 Rust 里面还有一个 crate 的概念,crate 是 Rust 最小的编译单元,在一个范围内将多个文件里面的功能组合在一起,最终通过编译生成一个二进制文件或库文件。所以 crate 是项目中多个文件的组合,整体形成一棵树,并且其中一个是入口文件、也就是它的根,对于当前来说显然是 main.rs。

因此我们还可以这么调用:

fn main() {
    crate::a::func2();
    /*
    我是公有函数 func2
    */
    crate::a::func3();
    /*
    我是公有函数 func3
    我是私有函数 func1
    */
}

通过 crate 即可从指定文件里面查找指定条目,并且这种方式相当于使用绝对路径进行查找,因此它永远都是成立的。另外可能有人发现了,这里 crate 后面没有指定文件啊,因为 main.rs 是入口文件,如果不指定文件的话,那么查找的默认是 main.rs 里的条目。

然后模块也是可以嵌套的,我们在模块 a 里面还可以继续定义模块 b。

mod a {
    pub mod b {
        pub fn func() {
            println!("我是模块 a/b 下的 func");
        }
    }
}
fn main() {
    // 想要调用里面的 func 函数
    // 那么模块 b 和 func 函数都必须是公有的
    a::b::func();
    /*
    我是模块 a/b 下的 func
    */
}

mod 内部的条目之间,也可以相互调用,举个例子:

mod a {
    pub fn mod_a_f1() {
        b::mod_b_f2()
    }
    // 注意:mod b 不是公有的
    mod b {
        pub fn mod_b_f2() {
            println!("我是模块 a/b 下的函数 f2")
        }
    }
}

fn main() {
    a::mod_a_f1();
    /*
    我是模块 a/b 下的函数 f2
    */

    // a::b::mod_b_f2(); // 不合法
}

我们在函数 mod_a_f1 的内部调用模块 b 的一个函数,显然整个过程不需要解释,但模块 b 不是公有的为啥也能访问呢?很简单,因为函数 mod_a_f1 和模块 b 是在同一级,所以可以直接拿到模块 b,因此模块 b 可以不是公有的,但它内部的函数必须公有。

而 main 函数和模块 b 显然就不是同一级了,它们之间有一个屏障,也就是模块 a。但模块 b 在模块 a 里面不是公有的,因此在 main 函数里面无法通过 a::b 的方式获取。

然后上面是在父模块内部调用子模块的条目,因此条目必须公有;但如果是子模块调用父模块的条目,那么条目是否公有就都无所谓了。

mod a {
    // 函数是私有的
    fn mod_a_f1() {
        println!("我是模块 a 下的函数 f1")
    }

    pub mod b {
        pub fn mod_b_f2() {
            println!("我是模块 a/b 下的函数 f2");
            // 想在这里调用 mod_a_f1 要怎么做呢?首先要找到模块 a,但它和 a 不在同一级
            // 因此无法使用 a::mod_a_f1() 的方式
            crate::a::mod_a_f1();
            // 需要使用绝对路径,从 crate 开始定位
        }
        pub mod c {
            pub fn mod_c_f3() {
                println!("我是模块 a/b/c 下的函数 f3");
                // 它是一个嵌套在模块 b 里面的模块
                // 如果也要调用 mod_a_f1,显然方法和上面相同
                // 但除此之外还有一种方式,就是使用 super
                super::super::mod_a_f1();
                // super 表示获取当前所在模块的上一级模块
                // 一个 super 获取到的显然是模块 b
                // 两个 super 获取到的就是模块 a 了
            }
        }
    }
}

fn main() {
    // 首先 main 函数里的 crate::a::mod_a_f1() 是不合法的;
    // 因为 mod_a_f1 在模块 a 的内部是不可见的

    // 但 mod_b_f2 里面也是 crate::a::mod_a_f1() 啊,为啥它就合法呢
    // 因为子 mod 中的条目如果私有,对于父 mod 是不可见的
    // 但父 mod 中的条目无论公有还是私有,子 mod 都是可见的
    // 所以模块 a 的 mod_a_f1,对于在 crate 里面的 main 函数来说不可见
    // 但对于在模块 b 里面的 mod_b_f2 来说是可见的
    // 因此调用方式是一样的,唯一的区别就是调用位置所导致的可见性问题
    a::b::mod_b_f2();
    /*
    我是模块 a/b 下的函数 f2
    我是模块 a 下的函数 f1
    */

    a::b::c::mod_c_f3();
    /*
    我是模块 a/b/c 下的函数 f3
    我是模块 a 下的函数 f1
    */
}

还是很好理解的,需要注意的是里面的 super。从父模块找子模块的话,直接一级一级往下找即可,但子模块找父模块则需要从 crate 开始,有时会比较麻烦。为此 Rust 提供了 super,用于定位上一级模块。

然后我们再来看一个好玩的:

mod a {
    pub mod b {
        pub mod c {
            pub fn mod_c_f3() {
                println!("我是模块 a/b/c 下的函数 f3");
                // super 表示上一级模块
                // super::super 显然就是上上一级,也就是模块 a
                // 那么 super::super::super 表示啥呢?显然是 crate
                // 而通过 crate 即可找到 main 函数和模块 a
                super::super::super::main();
            }
        }
    }
}

fn main() {
    println!("main 函数被调用");
    a::b::c::mod_c_f3();
}

你觉得这段代码执行的时候会发生什么现象呢?我们试一下。

我们看到因为无限递归导致栈溢出了,相信你应该明白模块之间的关系了,多个 rs 文件整体组成一个 crate,基于 crate 可以获取每一个文件的条目。此方法相当于使用绝对路径定位,因此无论在什么情况下它都是可靠的。但如果模块嵌套的比较深,那么通过 crate 一级一级查找就有点麻烦了,比如我们要获取相邻模块(比如上一级)内部的条目,这种情况下 Rust 推荐使用 super。

另外上面使用三个 super 找到了 crate,如果是四个 super 呢?显然会报错,因为 crate 已经是最顶层了。

结构体的可见性

结构体可见分为两部分,一个是结构体本身是否可见,另一个是字段是否可见。

结构体的 age 字段不是公有的,所以实例化的时候会报错。那如果实例化的时候不指定 age 会怎么样,答案是也会报错,因为 Rust 要求每一个字段都必须指定。所以如果你不希望某个字段被外界访问,那么就可以将其定义为私有,然后通过专门的方法进行实例化。

mod a {
    #[derive(Debug)]
    pub struct Girl {
        pub name: String,
        age: u8,
    }
    // 我们是为结构体实现方法,重点是方法
    // 所以 impl 的前面不需要 pub,也不能加 pub
    impl Girl {
        pub fn new(name: String, age: u8) -> Girl {
            Girl { name: name, age: age }
        }
    }
}
fn main() {
    let g = a::Girl::new(String::from("古明地觉"), 17);
    println!("{:?}", g);
    /*
    Girl { name: "古明地觉", age: 17 }
    */
}

new 方法也必须是公有的,否则在 main 里面无法调用。然后在创建结构体实例之后,也只能访问公有字段,不能访问私有字段。

枚举也是同样的道理,但枚举的成员不存在公有私有,只要枚举是公有的,那么内部的成员都可以访问。

mod a {
    #[derive(Debug)]
    pub enum Color {
        RGB(u8, u8, u8),
        HSV(u8, u8, u8)
    }
}
fn main() {
    let c = a::Color::RGB(133, 125, 89);
    println!("{:?}", c);
    /*
    RGB(133, 125, 89)
    */
}

还是比较简单的,然后我们这里使用条目的时候,都必须通过 模块:: 的方式,难免有些麻烦。于是我们可以使用 use 关键字将感兴趣的条目,引入到当前作用域。

use 声明

use关键字可以将指定的条目引入当前作用域,用于简化模块查找过程。

mod a {
    pub mod b {
        pub mod c {
            pub fn mod_c_f3() {
                println!("我是模块 a/b/c 下的函数 f3");
            }
        }
    }
}

fn main() {
    // 引入指定模块,这里通过绝对路径
    use crate::a::b;
    // 然后便可以通过 b 来进行查找
    b::c::mod_c_f3();  //我是模块 a/b/c 下的函数 f3

    // 引入模块,通过相对路径
    use a::b::c;
    c::mod_c_f3();  //我是模块 a/b/c 下的函数 f3

    // 还可以导入到某一个具体的函数
    use a::b::c::mod_c_f3;
    mod_c_f3();  //我是模块 a/b/c 下的函数 f3
}

所以当模块层级比较多的时候,我们还可以使用 use 将指定的模块单独导入进来,这样在使用的时候就没必要从最外层开始找了。当然啦,我们在导入的时候还可以起别名。

fn main() {
    use a::b::c as cc;
    cc::mod_c_f3();

    use a::b::c::mod_c_f3 as mod_c_f33333;
    mod_c_f33333();  
}

结果是一样的,总之在导入模块的时候,可以通过 as 起别名。

super 和 self

我们之前在查找条目的时候,使用了 super 关键字,它表示当前模块的上一级模块。然后除了 super,还有 self,它表示当前模块。

mod a {
    pub mod b {
        mod c {
            pub fn func1() {
                println!("我是 func1")
            }
        }
        // func2 所在的模块是 b
        pub fn func2() {
            // 两者是等价的
            // 但是使用 self,语义会更加的明确
            c::func1();
            self::c::func1();
        }
    }
}
fn main() {
    a::b::func2();
    /*
    我是 func1
    我是 func1
    */
}

从结果上没有区别,但通过 self 可以消除路径硬编码。

最后,我们上面都是手动定义一个模块,Rust 还可以导入文件,以及导入包。关于这方面的内容,可以点击阅读之前写过的一篇文章

错误处理

首先说一下错误(Error)和异常(Exception),有很多人分不清这两者的区别,我们来解释一下。在 Python 里面很少会对错误和异常进行区分,甚至将它们视做同一种概念。但在 Go 和 Rust 里面,错误和异常是完全不同的,异常要比错误严重得多。

当出现错误时,开发者是有能力解决的,比如文件不存在。这时候程序并不会有异常产生,而是正常执行,只是作为返回值的 error 不为空,开发者要基于 error 进行下一步处理。但如果出现了异常,那么一定是代码写错了,开发者无法处理了。比如索引越界,程序会直接 panic 掉,所以在 Rust 里面异常又叫做不可恢复的错误

不可恢复的错误

如果在 Rust 里面出现了异常,也就是不可恢复的错误,那么就表示开发者希望程序立刻中止掉,不要再执行下去了。而不可恢复的错误,除了程序在运行过程中因为某些原因自然产生之外,也可以手动引发,主要通过以下几个宏。

fn main() {
    // 调用 panic! 宏引发不可恢复错误,该宏支持字符串格式化
    panic!("发生了不可恢复的错误");

    // 调用 assert! 宏,当条件不满足时引发错误
    assert!(1 == 2);
    // 还有两个作用类似的宏
    // 等价于 assert!(1 == 2)
    assert_eq!(1, 2);
    // 等价于 assert!(1 != 2)
    assert_ne!(1, 2);

    // 当某个功能尚未实现时,一般使用该宏
    unimplemented!("还没开发完毕, by {}", "古明地觉");

    // 当程序执行到了一个不可能出现的位置时,使用该宏
    unreachable!("程序不可能执行到这里");
}

以上就是 Rust 里面的几个用于创建不可恢复的错误的几个宏,然后再来看看如何处理可恢复的错误,这是我们的重点。

可恢复的错误

可恢复的错误一般称之为错误,在 Go 里面错误是通过多返回值实现的,如果程序可能出现错误,那么会多返回一个 error,然后根据 error 是否为空来判断究竟有没有产生错误。所以开发者必须先对 error 进行处理,然后才可以执行下一步,不应该对 error 进行假设。

而 Rust 的错误机制和 Go 类似,只不过是通过枚举实现的,该枚举叫 Result,我们看一下它的定义。

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

如果将定义简化一下,那么就是这个样子。可以看到它就是一个简单的枚举,并且带有两个泛型。我们之前也介绍过一个枚举叫 Option,用来处理空值的,内部有两个成员,分别是 Some 和 None。

然后枚举 Result 和 Option 一样,它和内部的成员都是可以直接拿来用的,我们实际举个例子演示一下吧。

// 计算两个 i32 的商
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
    let ret: Result<i32, &'static str>;
    // 如果 b != 0,返回 Ok(a / b)
    if b != 0 {
        ret = Ok(a / b);
    } else {
        // 否则返回除零错误
        ret = Err("ZeroDivisionError: division by zero")
    }
    return ret;
}

fn main() {
    let a = divide(100, 20);
    println!("a = {:?}", a);

    let b = divide(100, 0);
    println!("b = {:?}", b);
    /*
    a = Ok(5)
    b = Err("ZeroDivisionError: division by zero")
    */
}

因为 Rust 返回的是枚举,比如上面代码中的 a 是一个 Ok(i32),即便没有发生错误,这个 a 也不能直接用,必须使用 match 表达式处理一下。

fn main() {
    // 将返回值和 5 相加,由于 a 是 Ok(i32)
    // 显然它不能直接和 i32 相加
    let a = divide(100, 20);
    match a {
        Ok(i) => println!("a + 5 = {}", i + 5),
        Err(error) => println!("出错啦: {}", error),
    }

    let b = divide(100, 0);
    match b {
        Ok(i) => println!("b + 5 = {}", i + 5),
        Err(error) => println!("出错啦: {}", error),
    }
    /*
    a + 5 = 10
    出错啦: ZeroDivisionError: division by zero
    */
}

虽然这种编码方式会让人感到有点麻烦,但它杜绝了出现运行时错误的可能。相比运行时报错,我们宁可在编译阶段多费些功夫。

unwrap

Rust 的错误通过 Result 枚举实现,里面有 Ok 和 Err 两个成员。如果没有发生错误,那么将值用 Ok 包装一下返回,如果发生错误了,那么将错误用 Err 包装返回。这样在拿到返回值的时候,使用 match 表达式进行处理。但说实话这样其实有点麻烦,如果返回的是 Ok(...),那么我们能不能直接把 Ok 里面的值拿到呢?答案是可以的,就是使用 unwrap。

fn test(age: u8) -> Result<String, String> {
    if age >= 18 {
        Ok(String::from("欢迎来到极乐净土"))
    } else {
        Err(String::from("未成年"))
    }
}

fn main() {
    let res = test(18);
    println!("{:?}", res);
    /*
    Ok("欢迎来到极乐净土")
    */
    let res = test(17);
    println!("{:?}", res);
    /*
    Err("未成年")
    */

    // 可以使用 match 拿到 Ok 里面的值
    // 但还有没有更简单的办法呢
    let res = test(20).unwrap();
    println!("{}", res);
    /*
    欢迎来到极乐净土
    */
}

Result 类型的值有一个 unwrap 方法,如果返回的是 Ok,那么会直接将值取出来。如果返回的 Err,那么会 panic 掉。

fn main() {
    let res: Result<&str, &str> = Ok("嘿嘿");
    println!("{}", res.unwrap());  // 嘿嘿

    let res: Result<&str, &str> = Err("哈哈");
    // 此处会直接 panic 掉
    println!("{}", res.unwrap());
}

Result 是可恢复的错误,通过 match 表达式可以对 Err 进行处理。但这样有些麻烦,于是可以通过 unwrap 直接将值拿出来,前提返回的是 Ok(...)。如果返回的是 Err(...),那么 unwrap 就会 panic 掉。

再比如字符串转整数:

fn main() {
    // 调用字符串的 parse 方法即可转化
    // 但转成什么类型呢?要通过 ::<T> 的方式指定
    let n = "23".parse::<i32>();
    // 返回的是 Result<i32, ParseIntError>
    // Ok 里面值的类型是什么,取决于我们要转成什么类型
    println!("{:?}", n);  // Ok(23)
    // 调用 unwrap,可以直接拿到 Ok 里面的值
    println!("{}", n.unwrap()); // 23

    // 但如果返回的不是 Ok(...) 呢
    let n = "你好".parse::<i32>();
    // 转化失败,但这是可恢复的错误,程序不会崩溃掉
    // 我们可以根据实际情况,进行合适的处理
    println!("{:?}", n);
    /*
    Err(ParseIntError { kind: InvalidDigit })
    */
    // 但如果调用 unwrap,那么当返回的是 Err 时,程序就会直接 panic 掉
    // println!("{}", n.unwrap()) // 此处会 panic 
}

因此当你能确保返回的是 Ok(...),那么直接 unwrap 即可,但也要承担因判断失误而引发 panic 的风险。

另外不光是 Result,Option 枚举也是可以这么做的。如果是 Some,调用 unwrap 会返回 Some 里面的值;如果是 None,调用 unwrap 则 panic。

fn main() {
    let n = Some(666).unwrap();
    println!("{}", n); // 666

    let n: Option<i32> = None;
    // 会 panic
    // println!("{}", n.unwrap());
}

所以通过 unwrap,我们能够简化代码的逻辑。

and_then 方法

Result 类型还提供了 and_then 方法,来看一下它的用法:

fn main() {
    let n1: Result<i32, &'static str> = Ok(123);
    // 如果是 Ok(...),那么将里面的值乘以 2
    // 如果是 Err(...),那么保持不变
    // 你也许会这么做
    let n2 = match n1 {
        Ok(val) => Ok(val * 2),
        Err(success) => Err(success),
    };
    println!("{:?}", n2);  // Ok(246)
    // 上面是一种做法,但还可以通过 and_then 进行简化
    // 如果 n1 是 Ok(...),那么会将 Ok 里面的值取出来,放到匿名函数当中调用
    let n2 = n1.and_then(|x: i32| { Ok(x * 2) });
    println!("{:?}", n2);  // Ok(246)

    // 如果 n1 是 Err(...)
    let n1 = Err("出错啦");
    // 那么不会执行 and_then,而是直接返回 Err(...)
    let n2 = n1.and_then(
        |x: i32| { println!("此处不会打印"); Ok(x * 2) }
    );
    println!("{:?}", n2);  // Err("出错啦")
}

之前在面对 Result 的时候,使用的是 match 表达式,但有时候不太方便,于是便有了 unwrap。Ok(...) 在调用 unwrap 的时候可以直接把值拿出来,但如果是 Err(...),则直接 panic。

于是现在又有了 and_then,它接收一个函数作为参数。and_then 相比 unwrap 的好处就在于,如果是 Err(...),那么程序不至于崩溃掉,而是直接把错误原封不动地返回。如果是 Ok(...) 调用 and_then,那么同样会将 Ok 里面的值拿出来,然后传到 and_then 接收的函数里面去进行调用,最后将它的返回值返回。

fn main() {
    let n1: Result<i32, &'static str> = Ok(6);
    // (6 * 2 + 1)^2
    let n2 = n1.and_then(|x| Ok(x * 2))
    .and_then(|x| Ok(x + 1))
    .and_then(|x| Ok(x * x));
    println!("{:?}", n2); // Ok(169)
}

我们再看一个更复杂的例子:

fn main() {
    let n1: Result<i32, &'static str> = Ok(6);
    let n2 = n1
        // 会将 Ok(6) 里面的 6 取出来
        // 传到 and_then 里面的函数进行执行
        .and_then(|x: i32| {
            println!("x * 2");
            Ok(x * 2)
        })
        // 同样的道理,但它返回的是 Err(...)
        .and_then(|x: i32| {
            println!("x + 1");
            Err("在 x + 1 这一步出错了")
        })
        // 因为上一步返回了 Err(...)
        // 所以此处的 and_then 不会执行
        .and_then(|x: i32| {
            println!("x * x");
            Ok(x * x)
        });
    println!("{:?}", n2);
    /*
    x * 2
    x + 1
    Err("在 x + 1 这一步出错了")
    */
}

相信你对 and_then 的用法已经充分了解了,如果你不关心程序是否 panic,或者确保它一定不会 panic,那么最简单的做法就是使用unwrap。但如果你无法保证,并且还希望出现 Err 的时候程序正常执行,那么使用 and_then,将处理逻辑写在一个函数里,然后作为参数传给 and_then。

另外不光 Result 可以使用 and_then,Option 也是可以的。

fn main() {
    let n1: Option<i32> = Some(6);
    // 要注意 and_then 里面函数的返回值类型
    // 调用它的 n1 是 Option 类型,所以函数也要返回 Option 类型
    let n2 = n1.and_then(|x| Some(x * 2));
    println!("{:?}", n2); // Some(12)

    // 调用 and_then 的如果是 Some(...),那么和 Ok 一样,会将 Some 里面的值取出来
    // 传到 and_then 接收的函数里面,进行调用
    // 但如果是 None 调用的 and_then,则直接返回 None
    let n2 = n1
        .and_then(|x| {
            println!("x * 2");
            Some(x * 2)
        })
        // 这里要指定返回值的类型,Option<T>
        // 因为出现了 None 的话会直接返回
        // 而只有一个 None,Rust 无法推断 T 的类型
        .and_then(|x| -> Option<i32> {
            println!("x + 1");
            None
        })
        .and_then(|x| {
            println!("x * x");
            Some(x * x)
        });
    println!("{:?}", n2);
    /*
    x * 2
    x + 1
    None
    */
}

所以在 and_then 方法的使用上,Result 和 Option 是类似的。如果是 Ok 或者 Some,那么将值取出来传到 and_then 接收的函数里面去;如果是 Err 或 None,那么直接返回,不会执行 and_then。

再举个例子,我们定义一个函数,接收两个字符串,转成 i32,计算它们的和。

use std::num::ParseIntError as E;
fn add(a: &str, b: &str) -> Result<i32, E> {
    // 解析成功,返回 Ok(a * b)
    // 解析失败,直接返回 Err(...),但注意这里的错误
    // 由于解析失败返回的是 ParseIntError
    // 因此 add 函数的错误类型也要是 ParseIntError
    a.parse::<i32>().and_then(|a| {
        b.parse::<i32>().and_then(|b| {
            Ok(a + b)
        })
    })
}

fn main() {
    println!("{:?}", add("12", "33"));
    println!("{:?}", add("a", "b"));
    /*
    Ok(45)
    Err(ParseIntError { kind: InvalidDigit })
    */
}

是不是很方便呢?有了 and_then,我们就可以不用 match 了。当然 match 虽然复杂了一些,但它的好处就是我们可以自定义错误处理逻辑。当然了,match 和 and_then 也可以结合起来使用。

然后我们上面为了避免函数定义过长,使用了取别名的做法,但取别名还有另一种方式。

use std::num::ParseIntError as E;
type ResultWithParseInt<T> = Result<T, E>;
fn add(a: &str, b: &str) -> ResultWithParseInt<i32> {
    a.parse::<i32>().and_then(|a| {
        b.parse::<i32>().and_then(|b| {
            Ok(a + b)
        })
    })
}

这种做法也是可以的。

最后除了 and_then,还有很多其它方法,比如 map, map_or, map_err 等等,可以了解一下。

问号表达式

先来回顾一下我们处理错误的几种方式:

type T = Result<i32, &'static str>;
// 使用 match
fn use_match(n: T) -> T {
    // 针对不同分支可以做出不同的处理
    // 包括 error 也可以自定制
    match n {
        Ok(i) => Ok(i * 2),
        Err(error) => Err(error)
    }
}

// 使用 unwrap
fn use_unwrap(n: T) -> T {
    // 如果 n 是 Err,那么此处直接 panic
    let i = n.unwrap();
    Ok(i * 2)
}

// 使用 and_then
fn use_and_then(n: T) -> T {
    // 如果 n 是 Err,那么错误原封不动返回
    n.and_then(|x| Ok(x * 2))
}

这些方式都有不完美的地方,match 和 and_then 不够简洁,至于 unwrap 虽然简单,但它会 panic。特别是当错误需要在上下文当中传递的时候,这三种方式都不够好,那么有没有更简单的做法呢,显然是有的。

首先 Rust 为了避免控制流混乱,并没有引入 try cache 语句。但 try cache 也有它的好处,就是可以完整地记录堆栈信息,从错误的根因到出错的地方,都能完整地记录下来,举个 Python 的例子:

程序报错了,根因是调用了函数 f,而出错的地方是在第 10 行,我们手动 raise 了一个异常。可以看到程序将整个错误的链路全部记录下来了,只要从根因开始一层层往下定位,就能找到错误原因。

而对于 Go 和 Rust 来说就不方便了,特别是 Go,如果每返回一个 error,就打印一次,那么会将 error 打的乱七八糟的。所以我们更倾向于错误能够在上下文当中传递,对于 Rust 而言,虽然 match 和 and_then 可以实现,但不够简洁。我们更推荐使用问号表达式来实现这一点。

fn external_some_func() -> Result<u32, &'static str> {
    // 外部的某个函数
    Ok(666)
}

fn call1() -> Result<f64, &'static str> {
    // 我们要调用 external_some_func
    match external_some_func() {
        // 类型转化在 Rust 里面通过 as 关键字
        Ok(i) => Ok((i + 1) as f64),
        Err(error) => Err(error)
    }
}

// 但是上面这种调用方式有点繁琐,我们还可以使用问号表达式
fn call2() -> Result<f64, &'static str> {
    // 注:使用问号表达式有一个前提
    // 调用方和被调用方的返回值都要是 Result 枚举类型
    // 并且它们的错误类型要相同,比如这里都是 &'static str
    let ret = external_some_func()?;
    Ok((ret + 1) as f64)
}

fn main() {
    println!("{:?}", call1());  // Ok(667.0)
    println!("{:?}", call2());  // Ok(667.0)
}

里面的 call1 和 call2 是等价的,如果在 call2 里面函数调用出错了,那么会自动将错误返回。并且注意 call2 里面的 ret,它是 u32,不是 Ok(u32)。因为函数调用出错会直接返回,不出错则会将 Ok 里面的 u32 取出来赋值给 ret。

所以问号表达式等价于不会 panic 的 unwrap,这也正是我们需要的,否则就要使用 match 表达式,或者调用 and_then 并往里面传入一个函数。对于只关心 Ok,而 Err 直接返回的场景来说,使用问号表达式是最合适的。

问号表达式完全可以使用 match 和 and_then 实现,但问号表达式无疑是最方便的。所以之前的一个例子:定义一个函数,接收两个字符串,转成 i32,计算它们的和,就可以这么改。

use std::num::ParseIntError as E;
// 使用 and_then
fn add1(a: &str, b: &str) -> Result<i32, E> {
    a.parse::<i32>().and_then(|a| {
        b.parse::<i32>().and_then(|b| {
            Ok(a + b)
        })
    })
}
// 使用问号表达式
fn add2(a: &str, b: &str) -> Result<i32, E> {
    Ok(a.parse::<i32>()? + b.parse::<i32>()?)
}

fn main() {
    println!("{:?}", add1("11", "22"));
    println!("{:?}", add2("11", "22"));
    /*
    Ok(33)
    Ok(33)
    */
    println!("{:?}", add1("a", "b"));
    println!("{:?}", add2("a", "b"));
    /*
    Err(ParseIntError { kind: InvalidDigit })
    Err(ParseIntError { kind: InvalidDigit })
    */
}

显然问号表达式是最方便的。

另外在 ? 出现以前,相同的功能是使用 try! 宏完成的,但是现在推荐使用 ? 表达式,不过在老代码中仍然会看到 try!。比如 try!(xxx()) 等价于 xxx()?

同时处理多种错误

再来考虑一种更复杂的情况,我们在调用函数的时候可能会调用多个函数,而这多个函数的错误类型不一样该怎么办呢?

#[derive(Debug)]
struct FileNotFoundError {
    err: String,
    filename: String,
}

#[derive(Debug)]
struct IndexError {
    err: &'static str,
    index: u32,
}

fn external_some_func1() -> Result<u32, FileNotFoundError> {
    Err(FileNotFoundError {
        err: String::from("文件不存在"),
        filename: String::from("main.py"),
    })
}

fn external_some_func2() -> Result<i32, IndexError> {
    Err(IndexError {
        err: "索引越界了",
        index: 9,
    })
}

很多时候,错误并不是一个简单的字符串,因为那样能携带的信息太少。基本上都是一个结构体,文字格式的错误信息只是里面的字段之一,而其它字段则负责描述更加详细的上下文信息。

我们上面有两个函数,是一会儿我们要调用的,但问题是它们返回的错误类型不同,也就是 Result<T, E> 里面的 E 不同。而如果是这种情况的话,问号表达式就会失效,那么我们应该怎么做呢?

// 其它代码不变
#[derive(Debug)]
enum MyError {
    Error1(FileNotFoundError),
    Error2(IndexError)
}

// 为 MyError 实现 From trait
// 分别是 From<FileNotFoundError> 和 From<IndexError>
impl From<FileNotFoundError> for MyError {
    fn from(error: FileNotFoundError) -> MyError {
        MyError::Error1(error)
    }
}

impl From<IndexError> for MyError {
    fn from(error: IndexError) -> MyError {
        MyError::Error2(error)
    }
}

fn call1() -> Result<i32, MyError>{
    // 调用的两个函数、和当前函数返回的错误类型都不相同
    // 但是当前函数是合法的,因为 MyError 实现了 From trait
    // 当错误类型是 FileNotFoundError 或 IndexError 时
    // 它们会调用 MyError 实现的 from 方法,然后将错误统一转换为 MyError 类型
    let x = external_some_func1()?;
    let y = external_some_func2()?;
    Ok(x as i32 + y)
}

fn call2() -> Result<i32, MyError>{
    let y = external_some_func2()?;
    let x = external_some_func1()?;
    Ok(x as i32 + y)
}

fn main() {
    println!("{:?}", call1());
    /*
    Err(Error1(FileNotFoundError { err: "文件不存在", filename: "main.py" }))
    */
    println!("{:?}", call2());
    /*
    Err(Error2(IndexError { err: "索引越界了", index: 9 }))
    */
}

如果调用的多个函数返回的错误类型相同,那么只需要保证调用方也返回相同的错误类型,即可使用问号表达式。但如果调用的多个函数返回的错误类型不同,那么这个时候调用方就必须使用一个新的错误类型,其数据结构通常为枚举。

而枚举里的成员要包含所有可能发生的错误类型,比如这里的 FileNotFoundError 和 IndexError。然后为枚举实现 From trait,该 trait 带了一个泛型,并且内部定义了一个 from 方法。我们在实现之后,当出现 FileNotFoundError 和 IndexError 的时候,就会调用 from 方法,转成调用方的 MyError 类型,然后返回。

因此这就是 Rust 处理错误的方式,可能有一些难理解,需要私下多琢磨琢磨。最后再补充一点,我们知道 main 函数应该返回一个空元组,但除了空元组之外,它也可以返回一个 Result。

fn main() -> Result<(), MyError> {
    // 如果 call1() 的后面没有加问号,那么在调用没有出错的时候,返回的就是 Ok(...)
    // 调用出错的时候,返回的就是 Err(...)。但不管哪一种,都是 Result<T, E> 类型
    println!("{:?}", call1());

    // 如果加了 ? 那么就不一样了
    // 在调用没出错的时候,会直接将 Ok(...) 里面的值取出来,调用出错的时候,当前函数会中止运行
    // 并将被调用方(这里是 call2)的错误作为调用方(这里是 main)的返回值返回
    // 此时通过问号表达式,就实现了错误在上下文当中传递
    // 所以这也要求被调用方返回的错误类型要和调用方相同
    println!("{:?}", call2()?);

    // 为了使函数签名合法,这里要返回一个值,直接返回 Ok(()) 即可
    // 但上面的 call2()? 是会报错的,所以它下面的代码都不会执行
    Ok(())
}

以上就是 Rust 的模块和错误处理,相比其它语言来说,确实难理解了一些。到目前为止,我们简单回顾了之前介绍的内容,后续开始学习新的内容。

posted @ 2023-10-17 15:26  古明地盆  阅读(146)  评论(0编辑  收藏  举报