rust 的枚举
在讲述Result或Option之前,我们有必要先了解一下rust的枚举;因为Result和Option都是枚举类型。
概念上rust的枚举与C语言的枚举是一致:定义一个类型,可以穷举所有可能的值。比如,定义一个IP地址类型:
enum IpAddrKind {
IPV4,
IPV6,
}
IP地址要么是IPV4, 要么是IPV6。 对于rust ip :IpAddrKind , ip 的值也是IPV4 或IPV6之一。
枚举类型在使用时,通常也需要和数据关联。在C语言中,当我们需要将枚举类型和和数据关联时,通常是额外定义一个结构体,将类型标识和数据封装在一起:
struct IpAddr {
enum IpAddrKind kind;
char data[16];
}
如此,当我们获得一个IpAddr实例时,可以根据其kind值先确认其地址类型;然后再根据其地址类型,从data中解析出具体数地址数据。
注意:这里data使用了共享存储的方法;原因是这里将IPv6 和IPv4的数据都定义为了字符数组,并且IPv6的长度可以覆盖IPv4。假如,重新定义IP的数据类型为 IPv4为一个U32整形,IPv6为一个字符数组,那么IpAddr的结构体可能会设计为这样:
struct IpAddr {
enum IpAddrKind kind;
union {
unsigned int ipv4;
char ipv6[16];
} data;
}
rust 相对于C的枚举,对枚举类型做了大幅优化,允许我们直接将关联数据类型直接嵌入到枚举的变体中。比如,rust定义的IpAddr 可能是这样:
enum IpAddr {
IPV4 (String),
IPV6 (String),
}
使用:
let loopback = IpAddr::IPV4("127.0.0.1".to_string()); // 定义了一个ipv4地址,其值“127.0.0.1”
简单起见,可以理解为rust 的枚举,融合了C枚举和联合体,实现了数据类型和关联数据的定义和绑定。
一个稍微复杂一点的枚举类型:
enum Message {
Quit, // 无绑定数据
Move {x: i32, y:i32}, // 绑定了一个匿名结构体 struct {x:i32, y:i32}
Write(string), // 绑定了一个字符串数据
ChangeColor(i32, i32, i32), // 绑定了一个元祖,由三个i32 组成
}
枚举方法
在rust 里面您还可以为枚举实现方法。这就像在面向对象编程时,为class (java)或结构体(rust, golang)绑定方法一样。和rust 的struct 实现方法一样,用impl关键字为指定的枚举类型添加方法:
impl Message {
fn call(&self) {
// do_something()
}
}
// example
let msg = Message::Write(String::from("notice: processing going down"));
msg.call();
枚举与match(控制流运输符号)
rust中有一个强大的控制流运算符:match,它允许将一个值与一些列模式进行匹配,并根据匹配的模式执行相关代码(关于rust的模式匹配,本文不深入,读者自行补充);而其中枚举是模式匹配中最为常用的:
impl Message {
fn call(&self) {
// do_something()
}
fn to_string(&self) -> String {
match self {
Message::Quit => String::from("quit"),
Message::Move { x, y } => format!("Move: <{},{}>", x, y),
Message::Write(s) => format!("String: {}", s),
Message::ChangeColor(x, y, z) => format!("ChangeColor: ({},{},{})", x, y, z),
}
}
}
上述示例中,为Message枚举实现的to_string方法返回某个具体示例的字符串值;其中就使用了match模式匹配。match 和C中的switch关键字比较类似,但比switch更为强大。
与其他语言不一样,rust在匹配枚举时,要求务必穷尽所有可能(当然,可以用通配的方法忽略不在意的变体)。
简单控制流 if let
前面已经提到,match 在遍历枚举时,要求务必穷尽所有可能。但有时候,我们确实只关注某一种匹配的情况,而忽略其他情况。当然,这种场景可以用match 的 '_' 通配的方式,来忽略其它不关心的变体,只是多写了了几行废代码而已。
幸运的是,rust 提供了一个if let 语法,可以简化这种场景的表达:
impl Message {
fn on_quit(&self) {
if let Message::Quit = self {
std::process::exit(0);
}
}
}
Option
在其他语言中,大部分都支持空值(Null,nil):本身是一个值,却表示‘没有值’。在支持空值的语言中,一个值可能处于两种状态:空值或非空值。
比如 c语言中,定义一个变量 char* ptr ,那么默认情况下ptr 就是空值(null)。
空值的问题在于,当你尝试像使用非空值那样去使用空值的时候,就会触发某种程度的错误(通常可能导致程序崩溃,比如访问空指针)。另一方面,因为这种存在双状态的值被广泛应用于程序中时,你很难避免引发类似问题。
但是,不管怎样,空值本身所尝试表达的概念任然具有意义:它代表了因某种原因而无法获取、或者变为无效的值。
rust语言中没有空值,但却提供了一个拥有类似概念的枚举:`Option<T>`。我们可以用它来表示任意一个可能存在空值的值。
enum Option<T> {
Some(T),
None,
}
在对待空值上,rust和其他支持空值的语言上有所差异。一般支持空值的语言,对于数据是否为空值,由程序员自己保证,语言上并不限制。但在rust 中`Option<T>` 包裹的值,需要特别处理。例如:
let x = 5;
let y: Option<i32> = Some(8);
let sum = x+y;
println!(“{}”, sum);
上面代码看上去没有问题,但实际上却无法通过编译。编译器指出,i32 和`Option<T>`,不支持相加行为,因为他们是不同类型。
rust中,对于一个 给定类型的变量(基础类型或者结构体),例子中的x,编译器保证它是有效的;但相反,一个`Option<T>`的变量,rust要求我们必须确认它是具有值的情况下,才可以使用。
换句话说,`Option<T> `中可能存在T,也可能是空值;我们必须确认它有值,并且将其转换为T才能够使用它。经过这个过程,就帮助我们甄别了值是否真实存在,从而避免了“使用了一个值,但它却是空值”的陷阱!
let x = 5;
let y: Option<i32> = Some(8);
let sum: i32 = 0;
match y {
Some(t) => {
sum = x+t; // 确保有值,并使用该值
},
- => (),
}
println!("sum={}", sum)
使用模式匹配来处理返回值,调用者必须处理结果为None的情况。这往往是一个好的编程习惯,可以减少潜在的bug。Option 包含一些方法来简化模式匹配,毕竟过多的match会使代码变得臃肿,这也是滋生bug的原因之一。
unwrap
impl<T> Option<T> {
fn unwrap(self) -> T {
match self {
Option::Some(val) => val,
Option::None => {
panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
unwrap 是Option的一个工具函数。当遇到None值时会panic。
通常panic 并不是一个良好的工程实践,不过有些时候却非常有用:
- 在例子和简单快速的编码中 有的时候你只是需要一个小例子或者一个简单的小程序,输入输出已经确定,你根本没必要花太多时间考虑错误处理,使用unwrap变得非常合适。
- 当程序遇到了致命的bug,panic是最优选择
map
pub fn map<U, F>(self, f: F) -> Option<U>
where
F: FnOnce(T) -> U,
{
match self {
Some(x) => Some(f(x)),
None => None,
}
}
map 是Option的一个工具函数:对一个Option类型的值,如果其值非空,那么通过一个映射函数,映射为一个新类型;否则返回为None。
假如我们要在一个字符串中找到文件的扩展名,比如foo.rs中的rs, 我们可以这样:
fn extension_explicit(file_name: &str) -> Option<&str> {
match find(file_name, '.') {
None => None,
Some(i) => Some(&file_name[i+1..]),
}
}
fn main() {
match extension_explicit("foo.rs") {
None => println!("no extension"),
Some(ext) => assert_eq!(ext, "rs"),
}
}
// 使用map去掉match
fn extension(file_name: &str) -> Option<&str> {
find(file_name, '.').map(|i| &file_name[i+1..])
}
注意上面 “|i| &file_name[i+1..]” 的写法是一个闭包函数。关于rust的闭包函数,请读者自行了解学习。
unwrap_or
fn unwrap_or<T>(option: Option<T>, default: T) -> T {
match option {
None => default,
Some(value) => value,
}
}
unwrap_or 提供了一个默认值default,当值为None时返该默认值。
and_then
fn and_then<F, T, A>(option: Option<T>, f: F) -> Option<A>
where F: FnOnce(T) -> Option<A> {
match option {
None => None,
Some(value) => f(value),
}
}
看起来and_then和map差不多, 当Option 非空时调用f函数,对传输数据进行处理,否则返回None。
与map的差异一方面是语义上的差异,map侧重于映射,而and_then表达丰富的后续处理;另一方面,在返回类型上and_then不限制,而map 保持输入和输出一致。可以认为,map 是and_then的一种特例。
Result
编程实践中,对于程序中的错误,通常分为两类: 不可恢复的错误 和可以恢复的错误。对于可恢复的错误,比如文件未找到,一般是报告给用户,让其重试;而不可恢复错误,比如数组访问越界了,则会引起程序进入异常状态。
在有异常处理的编程语言中,通常并不详细区分这两种错误,而是统一交由异常处理机制处理。rust没有异常处理机制,通常对于不可恢复错误,会采用panic结束程序;而对于可恢复错误,则更倾向通过显示的机制进行错误捕获和传递。对于可恢复错误,rust采用Result类型来描述。
Result 是一个枚举,有Ok 和 Err两个变体:
enum Result<T, E> {
Ok(T),
Err(E),
}
其中,T和E均为泛型类型。
有了前两节的知识铺垫,理解这个枚举并不困难,可以描述为:
1. 一个可能处理失败的过程,其结果用Result来表示;
2. 如果处理成功,那么返回Result的Ok 变体,并且携带返回数据;
3. 如果处理失败,那么返回Result的Err变体,并且携带错误信息。
示例:
let ret = File::open("test.txt");
let f = match ret {
Ok(file) => file,
Err(err) = {
panic!("fail to open test.txt, error: {:?}", err );
}
}
// f.XXXX()
工具函数
Result 和Option 非常相似,甚至可以理解为,Result是Option更为通用的版本,在异常的时候,返回了更多的错误信息;而Option 只是Result Err 为空的特例。
type Option<T> = Result<T, ()>;
和Option一样,Result 也提供了 unwrap,unwrap_or, map,and_then 等系列工具方法。比如 unwarp实现:
impl<T, E: ::std::fmt::Debug> Result<T, E> {
fn unwrap(self) -> T {
match self {
Result::Ok(val) => val,
Result::Err(err) =>
panic!("called `Result::unwrap()` on an `Err` value: {:?}", err),
}
}
}
没错和Option一样,不同的是,Result包括了错误的详细描述,这对于调试人员来说,这是友好的。
除此之外,相比于Option, Result也有一些特有的针对错误类型的方法map_err和or_else等。
其中:
map_err 处理一个Result,当前是某种错误类型时,通过传入的op方法,转换其错误类型; 如果是非错误类型,则不受影响。
pub fn map_err<F, O: FnOnce(E) -> F>(self, op: O) -> Result<T, F> {
match self {
Ok(t) => Ok(t),
Err(e) => Err(op(e)),
}
}
or_else 处理一个Result并返回一个Result,当前是某种错误时,通过传入的op方法,处理错误;如果是非错误类型,则不受影响。
pub fn or_else<F, O: FnOnce(E) -> Result<T, F>>(self, op: O) -> Result<T, F> {
match self {
Ok(t) => Ok(t),
Err(e) => op(e),
}
}
or_else 通常用于链式调用的流程控制。例如:
fn auto_fix(e: u32) -> Result<u32, u32> { Ok(e * e) }
fn keep(e: u32) -> Result<u32, u32> { Err(e) }
// 用例1和2,由于 原始Result值非 错误,所以不受or_else影响
assert_eq!(Ok(2).or_else(auto_fix).or_else(auto_fix), Ok(2));
assert_eq!(Ok(2).or_else(keep).or_else(auto_fix), Ok(2));
// 用例3, Err类型的Result 经过auto_fix 后已经转为Ok(9);经过第二个or_else 不受影响
assert_eq!(Err(3).or_else(auto_fix).or_else(keep), Ok(9));
// 用例4, Err类型的Result 连续调用or_else 的keep,由于keep实现保留err返回为Err(3); 注意实际上Result实例时变化了的
assert_eq!(Err(3).or_else(keep).or_else(keep), Err(3));
Result别名
在Rust的标准库中会经常出现Result的别名,用来默认确认其中Ok(T)或者Err(E)的类型,这能减少重复编码。比如io::Result
use std::num::ParseIntError;
use std::result;
type Result<T> = result::Result<T, ParseIntError>;
fn double_number(number_str: &str) -> Result<i32> {
unimplemented!();
}
组合Option和Result
Option的方法ok_or:
fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> {
match option {
Some(val) => Ok(val),
None => Err(err),
}
}
可以在值为None的时候返回一个Result::Err(E),值为Some(T)的时候返回Ok(T),利用它我们可以组合Option和Result:
use std::env;
fn double_arg(mut argv: env::Args) -> Result<i32, String> {
argv.nth(1)
.ok_or("Please give at least one argument".to_owned())
.and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
.map(|n| 2 * n)
}
fn main() {
match double_arg(env::args()) {
Ok(n) => println!("{}", n),
Err(err) => println!("Error: {}", err),
}
}
double_arg将传入的命令行参数转化为数字并翻倍,ok_or将Option类型转换成Result,map_err当值为Err(E)时调用作为参数的函数处理错误。
try! 宏
macro_rules! try {
($e:expr) => (match $e {
Ok(val) => val,
Err(err) => return Err(::std::convert::From::from(err)),
});
}
try!事实上就是match Result的封装,当遇到Err(E)时会提早返回, ::std::convert::From::from(err)可以将不同的错误类型返回成最终需要的错误类型,因为所有的错误都能通过From转化成`Box<Error>`,所以下面的代码是正确的:
use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::path::Path;
fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Box<Error>> {
let mut file = try!(File::open(file_path));
let mut contents = String::new();
try!(file.read_to_string(&mut contents));
let n = try!(contents.trim().parse::<i32>());
Ok(2 * n)
}
在新版本中 try!宏被进一步简化为 一个?:
fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Error> {
let mut file = File::open(file_path)?; // 注意这里的?, 和try功能一致,遇到错误,提前返回
let mut contents = String::new();
try!(file.read_to_string(&mut contents));
let n = try!(contents.trim().parse::<i32>());
Ok(2 * n)
}
总结
rust的Option 和Result 为返回、检测、处理错误,提供了系统支撑,这一点和golang的errors 设计比价类似。
熟练使用Option和Result是编写 Rust 代码的关键,Rust 优雅的错误处理离不开值返回的错误形式,编写代码时提供给使用者详细的错误信息是值得推崇的。