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 的模块和错误处理,相比其它语言来说,确实难理解了一些。到目前为止,我们简单回顾了之前介绍的内容,后续开始学习新的内容。
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏