Rust是一种注重安全和性能的系统编程语言,并连续六年在Stack Overflow的年度调查中被评为 "最受喜爱的语言"!Rust是一种非常有趣的语言。Rust的编程之所以如此有趣,其中一个原因是,尽管它专注于性能,但它有很多深思熟虑的便利,这些便利经常与高级语言相关。
这些便利之一是使用枚举,特别是Option 和Result 类型。
Option 类型
Rust的nullable类型的版本是Option<T> 类型。它是一个枚举类型(在其他一些语言中也被称为代数数据类型),其中每个实例都是要么。
None- 或
Some(value)
这时,value 可以是任何类型的值T 。例如,Vec<T> 是Rust的类型,代表一个向量(或可变大小的数组)。它有一个pop() 方法,可以返回一个Option<T> ,如果向量是空的,那么这个None ,或者Some(value) ,包含向量的最后一个值。
一个返回Option 的API的好处之一是,为了获得里面的值,调用者不得不检查该值是否是None 。这就避免了在其他没有nullable类型的语言中的问题。
例如,在C++中,std::find() 返回一个迭代器,但是你必须记得检查它以确保它不是容器的end()- 如果你忘记了这个检查并试图从容器中获取项目,你会得到未定义的行为。
缺点是,这往往会使代码变得令人讨厌的冗长。但是,Rust有很多技巧可以帮助你
使用expect 和unwrap
如果你确定一个Option 里面有一个真实的值,那么expect() 和unwrap() 是为你准备的!它们返回里面的值,但如果该变量实际上是None ,你的程序就会退出。(这就是所谓的惊慌失措,有些情况下是可以恢复的,但为了简单起见,我们在此略过)。
唯一的区别是,expect() 可以让你指定一个自定义的信息,在程序退出时打印到控制台。
还有一个unwrap_or() ,它让你指定一个默认值,如果该值是None ,那么Some(5).unwrap_or(7) 就是5 ,None.unwrap_or(7) 就是7 。
如果你愿意,你可以在像这样调用unwrap() 之前检查Option<T> 是否有一个值。
// t is an Option<T>
if t.is_some() {
let real_value = t.unwrap();
}
但是,还有更简洁的方法来做到这一点(例如,使用if let ,我们将在后面介绍)。
使用match
查看一个Option 是否有一个值的最基本的方法是使用模式匹配和一个match 表达式。这适用于任何枚举类型,看起来像这样。
// t is an Option<T>
match t {
None => println!("No value here!"), // one match arm
Some(x) => println!("Got value {}", x) // the other match arm
};
有一点需要注意的是,Rust编译器强制要求match 表达式必须是详尽的;也就是说,每个可能的值都必须被匹配臂覆盖。所以,下面的代码不会被编译。
// t is an Option<T>
match t {
Some(x) => println!("Got value {}", x)
};
而且我们会得到一个错误。
error[E0004]: non-exhaustive patterns: `None` not covered
这实际上是非常有帮助的,可以避免当你认为你已经覆盖了所有的情况,但却没有覆盖的时候如果你明确地想忽略所有其他情况,你可以使用_ match 表达式。
// t is an Option<T>
match t {
Some(x) => println!("Got value {}", x), // the other match arm
_ => println!("OK not handling this case.");
};
使用if let
只有当一个Option 有一个真实的值时,才想做一些事情,这是很常见的,而if let 是一种简洁的方法,可以将做这些事情与获得基本值结合起来。
例如,如果t 有一个值,下面的代码将打印"Got <value>" ,如果t 是None ,则不做任何事情。
// t is an Option<T>
if let Some(i) = t {
println!("Got {}", i);
}
if let 实际上对任何枚举类型都有效!
使用map
也有一些方法可以在不检查它是否有值的情况下对Option<T> 。例如,你可以使用map() ,如果它有一个真实的值,就把它转换为None 。
因此,例如,Some(10).map(|i| i + 1) 是Some(11) ,None.map(|i| i + 1) 仍然是None 。
使用into_iter 与Option
如果你有一个Vec<Option<T>> ,你可以把它转换成一个Option<Vec<T>> ,如果原向量中的任何条目是None ,它将是None 。
如果你考虑接收许多操作的结果,并且你希望如果任何一个单独的操作失败,整体的结果就会失败,这样做是有意义的。
因此,例如vec![Some(10), Some(20)].into_iter().collect() 是Some([10, 20])
而vec![Some(10), Some(20), None].into_iter().collect() 是None 。
Result 类型
Rust的Result<T, E> 类型是一种返回值或错误的方便方法。和Option 类型一样,它是一个枚举类型,有两种可能的变体。
Ok(T), 表示操作成功了,有值TErr(E),表示操作失败,有一个错误E
知道如果一个函数返回一个错误,它将是这种类型,这是非常方便的,而且有一堆有用的方法来使用它们!
使用ok_or
既然Option 和Result 如此相似,有一个简单的方法可以在两者之间进行转换。Option 有ok_or() 方法:Some(10).ok_or("uh-oh") 是Ok(10) ,None.ok_or("uh-oh") 是Err("uh-oh") 。
然后,Result 有ok()的方法:Ok(10).ok() 是Some(10) ,Err("uh-oh").ok() 是None 。
在Result 上还有一个err() 方法,它的作用正好相反:错误被映射到Some ,成功值被映射到None 。
使用expect,unwrap,match, 和if let
就像使用Option ,如果你确定Result 是成功的(如果你错了也不介意退出!),expect() 和unwrap() 的工作方式与Option 完全一样。
而且,由于Result 是一个枚举类型,match 和if let 也以同样的方式工作!
使用? 操作符
好了,这就是事情变得非常酷的地方。假设你正在编写一个返回Result 的函数,因为它可能失败,而你正在调用另一个返回Result 的函数,因为它可能失败。
很多时候,如果另一个函数返回一个错误,你想直接从该函数中返回该错误。所以,你的代码会像下面这样。
let inner_result = other_function();
if let Err(some_error) = inner_result {
return inner_result;
}
let real_result = inner_result().unwrap();
// now real_result has the actual value we care about, we can continue on...
但是,这样一遍又一遍地写是很麻烦的。相反,你可以写这样的代码。
let real_result = other_function()?;
这就对了:单一的? 操作符就可以做到这一切!更棒的是,你可以把调用连在一起,像这样。
let real_result = this_might_fail()?.also_might_fail()?.this_one_might_fail_too()?;
另一种常见的技术是使用类似map_err() 的东西将错误转化为对外层函数更有意义的返回,然后使用? 操作符。
使用must_use
Rust编译器的帮助是出了名的,它的帮助方式之一就是警告你可能犯的错误。
Result 类型被标记为must_use 属性,这意味着如果一个函数返回一个Result ,调用者不能忽略这个值,否则编译器会发出警告。
这主要是那些没有真实值返回的函数的问题,比如I/O函数;许多函数返回的类型是Result<(), Err> (() 被称为单元类型),在这种情况下,很容易忘记检查错误,因为没有成功值可以得到。
但是,编译器是为了帮助你记住!
将into_iter 与Result
类似于Option ,如果你有一个Vec<Result<T, E>> ,你可以使用into_iter() 和collect() 将其转化为一个Result<Vec<T>, E> ,它将包含所有的成功值或遇到的第一个错误。
因此,举例来说,下面是Ok([10, 20]) 。
vec![Ok(10), Ok(20)].into_iter().collect()
然后,这就是Err("bad") 。
vec![Ok(10), Err("bad"), Ok(20), Err("also bad")].into_iter().collect()
如果你想收集所有的错误,而不是只收集第一个错误,那就有点麻烦了,但你可以使用方便的partition() ,把成功和错误分开。
let v: Vec<Result<_, _>> = some_other_function();
let (successes, errors): (Vec<_>, Vec<_>) = v.into_iter().partition(Result::is_ok);
if !errors.is_empty() {
return Err(errors.into_iter().map(Result::unwrap_err).collect());
}
else {
return Ok(successes.into_iter().map(Result::unwrap).collect());
}
结论
Option 和Result 背后的想法对Rust来说并不新鲜。对我来说,最突出的是这门语言通过检查错误来做正确的事情是多么容易,特别是通过? 操作符。