Rust处理空值
Option
是标准库定义的另一个枚举。Option
类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。
例如,如果请求一个非空列表的第一项,会得到一个值,如果请求一个空的列表,就什么也不会得到。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。
编程语言的设计经常要考虑包含哪些功能,但考虑排除哪些功能也很重要。Rust
并没有很多其他语言中有的空值功能。空值(Null )
是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
为此,Rust
并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>
,而且它定义于标准库中,如下:
enum Option<T> {
None,
Some(T),
}
Option<T>
枚举是如此有用以至于它甚至被包含在了 prelude
之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要 Option::
前缀来直接使用Some
和 None
。即便如此 Option<T>
也仍是常规的枚举,Some(T)
和 None
仍是 Option<T>
的成员。
<T>
语法是一个我们还未讲到的Rust
功能。它是一个泛型类型参数。目前,所有你需要知道的就是 <T> 意味着 Option
枚举的 Some
成员可以包含任意类型的数据,同时每一个用于T
位置的具体类型使得Option<T>
整体作为不同的类型。这里是一些包含数字类型和字符串类型 Option
值的例子:
let some_number = Some(5);
let some_char = Some('e');
// 空值
let absent_number: Option<i32> = None;
some_number
的类型是Option<i32>
。some_char
的类型是 Option<char>
,这(与 some_number
)是一个不同的类型。因为我们在 Some
成员中指定了值,Rust
可以推断其类型。对于 absent_number
,Rust
需要我们指定 Option
整体的类型,因为编译器只通过None
值无法推断出 Some
成员保存的值的类型。这里我们告诉Rust
希望 absent_number
是Option<i32>
类型的。
当有一个 Some
值时,我们就知道存在一个值,而这个值保存在 Some
中。当有个None
值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么,Option<T>
为什么就比空值要好呢?
简而言之,因为 Option<T>
和T
(这里 T
可以是任何类型)是不同的类型,编译器不允许像一个肯定有效的值那样使用 Option<T>
。例如,这段代码不能编译,因为它尝试将 Option<i8>
与 i8
相加:
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
}
如果运行这些代码,将得到类似这样的错误信息:
$ cargo run
Compiling variables v0.1.0 (/projects/variables)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
<&'a i8 as Add<i8>>
<&i8 as Add<&i8>>
<i8 as Add<&i8>>
<i8 as Add>
For more information about this error, try `rustc --explain E0277`.
error: could not compile `variables` due to previous error
错误信息意味着 Rust
不知道该如何将 Option<i8>
与 i8
相加,因为它们的类型不同。当在 Rust
中拥有一个像 i8
这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。只有当使用 Option<i8>
(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。
比如我们想要编写一个函数,它获取一个Option<i32>
,如果其中含有一个值,将其加一。如果其中没有值,函数应该返回 None
值,而不尝试执行任何操作:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
fn main() {
let six = plus_one(Some(5));
let none = plus_one(None);
println!("six = {:?} , none = {:?}", six, none);
}
运行,输出:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/variables`
six = Some(6) , none = None
if let
语法让我们以一种不那么冗长的方式结合 if
和 let
,来处理只匹配一个模式的值而忽略其他模式的情况:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
fn main() {
// 只处理有值的情况
if let Some(v) = plus_one(Some(1)) {
println!("v = {}", v);
}
// 考虑None的情况
if let Some(v) = plus_one(None) {
println!("v = {}", v);
} else {
println!("None");
}
}
if let
语法获取通过等号分隔的一个模式和一个表达式。它的工作方式与 match
相同,这里的表达式对应 match
而模式则对应第一个分支。在这个例子中,模式是 Some(v)
,v
绑定为 Some
中的值。接着可以在 if let
代码块中使用 v
了,就跟在对应的 match
分支中一样。模式不匹配时 if let
块中的代码不会执行。
使用 if let
意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去 match
强制要求的穷尽性检查。match
和if let
之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!