了解Rust的选项和结果枚举

284 阅读7分钟

Rust是一种注重安全和性能的系统编程语言,并连续六年在Stack Overflow的年度调查中被评为 "最受喜爱的语言"!Rust是一种非常有趣的语言。Rust的编程之所以如此有趣,其中一个原因是,尽管它专注于性能,但它有很多深思熟虑的便利,这些便利经常与高级语言相关。

这些便利之一是使用枚举,特别是OptionResult 类型。

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有很多技巧可以帮助你

使用expectunwrap

如果你确定一个Option 里面有一个真实的值,那么expect()unwrap() 是为你准备的!它们返回里面的值,但如果该变量实际上是None ,你的程序就会退出。(这就是所谓的惊慌失措,有些情况下是可以恢复的,但为了简单起见,我们在此略过)。

唯一的区别是,expect() 可以让你指定一个自定义的信息,在程序退出时打印到控制台。

还有一个unwrap_or() ,它让你指定一个默认值,如果该值是None ,那么Some(5).unwrap_or(7) 就是5None.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>" ,如果tNone ,则不做任何事情。

// 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_iterOption

如果你有一个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), 表示操作成功了,有值T
  • Err(E),表示操作失败,有一个错误E

知道如果一个函数返回一个错误,它将是这种类型,这是非常方便的,而且有一堆有用的方法来使用它们!

使用ok_or

既然OptionResult 如此相似,有一个简单的方法可以在两者之间进行转换。Optionok_or() 方法:Some(10).ok_or("uh-oh")Ok(10)None.ok_or("uh-oh")Err("uh-oh")

然后,Resultok()的方法: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 是一个枚举类型,matchif 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_iterResult

类似于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());
}

结论

OptionResult 背后的想法对Rust来说并不新鲜。对我来说,最突出的是这门语言通过检查错误来做正确的事情是多么容易,特别是通过? 操作符。