@ -2,19 +2,16 @@
> [ch06-01-defining-an-enum.md ](https://github.com/rust-lang/book/blob/main/src/ch06-01-defining-an-enum.md )
> < br >
> commit a5a03d8f61a5b2c2111b21031a3f526ef60844dd
> commit d3740fb7aad0ea4a80ae20f64dee3a8cfc0c5c3c
让我们看看一个需要诉诸于代码的场景,来考虑为何此时使用枚举更为合适且实用。假设我们要处理 IP 地址。目前被广泛使用的两个主要 IP 标准: IPv4( version four) 和 IPv6( version six) 。这是我们的程序可能会遇到的所有可能的 IP 地址类型:所以可以 ** 枚举** 出所有可能的值,这也正是此枚举名字的由来。
枚举是一个不同于结构体的定义自定义数据库类型的方式。 让我们看看一个需要诉诸于代码的场景,来考虑为何此时使用枚举更为合适且实用。假设我们要处理 IP 地址。目前被广泛使用的两个主要 IP 标准: IPv4( version four) 和 IPv6( version six) 。这是我们的程序可能会遇到的所有可能的 IP 地址类型:所以可以 ** 枚举** 出所有可能的值,这也正是此枚举名字的由来。
任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的, 而且不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景, 因为枚举值只可能是其中一个成员。IPv4 和 IPv6 从根本上讲仍是 IP 地址,所以当代码在处理适用于任何类型的 IP 地址的场景时应该把它们当作相同的类型。
可以通过在代码中定义一个 `IpAddrKind` 枚举来表现这个概念并列出可能的 IP 地址类型,`V4` 和 `V6` 。这被称为枚举的 ** 成员**( *variants*) :
```rust
enum IpAddrKind {
V4,
V6,
}
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-01-defining-enums/src/main.rs:def}}
```
现在 `IpAddrKind` 就是一个可以在代码中使用的自定义数据类型了。
@ -24,62 +21,25 @@ enum IpAddrKind {
可以像这样创建 `IpAddrKind` 两个不同成员的实例:
```rust
# enum IpAddrKind {
# V4,
# V6,
# }
#
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-01-defining-enums/src/main.rs:instance}}
```
注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在 `IpAddrKind::V4` 和 `IpAddrKind::V6` 都是 `IpAddrKind` 类型的。例如,接着可以定义一个函数来获取任何 `IpAddrKind` :
```rust
# enum IpAddrKind {
# V4,
# V6,
# }
#
fn route(ip_type: IpAddrKind) { }
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-01-defining-enums/src/main.rs:fn}}
```
现在可以使用任一成员来调用这个函数:
```rust
# enum IpAddrKind {
# V4,
# V6,
# }
#
# fn route(ip_type: IpAddrKind) { }
#
route(IpAddrKind::V4);
route(IpAddrKind::V6);
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-01-defining-enums/src/main.rs:fn_call}}
```
使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个存储实际 IP 地址 ** 数据** 的方法;只知道它是什么 ** 类型** 的。考虑到已经在第五章学习过结构体了,你可能会像示例 6-1 那样处理这个问题:
```rust
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-01/src/main.rs:here}}
```
< span class = "caption" > 示例 6-1: 将 IP 地址的数据和 `IpAddrKind` 成员存储在一个 `struct` 中</ span >
@ -89,35 +49,19 @@ let loopback = IpAddr {
我们可以使用一种更简洁的方式来表达相同的概念,仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。`IpAddr` 枚举的新定义表明了 `V4` 和 `V6` 成员都关联了 `String` 值:
```rust
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-02-enum-with-data/src/main.rs:here}}
```
我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。
我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。这里也很容易看出枚举工作的另一个细节:每一个我们定义的枚举成员的名字也变成了一个构建枚举的实例的函数。也就是说,`IpAddr::V4()` 是一个获取 `String` 参数并返回 `IpAddr` 类型实例的函数调用。作为定义枚举的结果,这些构造函数会自动被定义。
用枚举替代结构体还有另一个优势: 每个成员可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将 `V4` 地址存储为四个 `u8` 值而 `V6` 地址仍然表现为一个 `String` ,这就不能使用结构体了。枚举则可以轻易的处理这个情况:
```rust
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-03-variants-with-different-data/src/main.rs:here}}
```
这些代码展示了使用枚举来存储两种不同 IP 地址的几种可能的选择。然而,事实证明存储和编码 IP 地址实在是太常见了[以致标准库提供了一个开箱即用的定义!][IpAddr]<!-- ignore --> 让我们看看标准库是如何定义 `IpAddr` 的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员中的地址数据嵌入到了两个不同形式的结构体中,它们对不同的成员的定义是不同的:
[IpAddr]: https://doc.rust-lang.org/std/net/enum.IpAddr.html
```rust
struct Ipv4Addr {
// --snip--
@ -140,12 +84,7 @@ enum IpAddr {
来看看示例 6-2 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型:
```rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-02/src/main.rs:here}}
```
< span class = "caption" > 示例 6-2: 一个 `Message` 枚举,其每个成员都存储了不同数量和类型的值</ span >
@ -153,20 +92,14 @@ enum Message {
这个枚举有四个含有不同类型的成员:
* `Quit` 没有关联任何数据。
* `Move` 包含一个匿名结构体 。
* `Move` 类似结构体包含命名字段 。
* `Write` 包含单独一个 `String` 。
* `ChangeColor` 包含三个 `i32` 。
定义一个如示例 6-2 中所示那样的有关联值的枚举的方式和定义多个不同类型的结构体的方式很相像,除了枚举不使用 `struct` 关键字以及其所有成员都被组合在一起位于 `Message` 类型下。如下这些结构体可以包含与之前枚举成员中相同的数据:
```rust
struct QuitMessage; // 类单元结构体
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-04-structs-similar-to-message-enum/src/main.rs:here}}
```
不过,如果我们使用不同的结构体,由于它们都有不同的类型,我们将不能像使用示例 6-2 中定义的 `Message` 枚举那样,轻易的定义一个能够处理这些不同类型的结构体的函数,因为枚举是单独一个类型。
@ -174,21 +107,7 @@ struct ChangeColorMessage(i32, i32, i32); // 元组结构体
结构体和枚举还有另一个相似点:就像可以使用 `impl` 来为结构体定义方法那样,也可以在枚举上定义方法。这是一个定义于我们 `Message` 枚举上的叫做 `call` 的方法:
```rust
# enum Message {
# Quit,
# Move { x: i32, y: i32 },
# Write(String),
# ChangeColor(i32, i32, i32),
# }
#
impl Message {
fn call(& self) {
// 在这里定义方法体
}
}
let m = Message::Write(String::from("hello"));
m.call();
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-05-methods-on-enums/src/main.rs:here}}
```
方法体使用了 `self` 来获取调用方法的值。这个例子中,创建了一个值为 `Message::Write(String::from("hello"))` 的变量 `m` ,而且这就是当 `m.call()` 运行时 `call` 方法中的 `self` 的值。
@ -197,7 +116,7 @@ m.call();
### `Option` 枚举和其相对于空值的优势
在之前的部分,我们看到了 `IpAddr` 枚举如何利用 Rust 的类型系统在程序中编码更多信息而不单单是数据。接下来我们 分析一个 `Option` 的案例,`Option` 是标准库定义的另一个枚举。`Option` 类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。
这一部分会 分析一个 `Option` 的案例,`Option` 是标准库定义的另一个枚举。`Option` 类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。例如,如果请求一个包含项的列表的第一个值,会得到一个值,如果请求一个空的列表,就什么也不会得到 。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。
编程语言的设计经常要考虑包含哪些功能, 但考虑排除哪些功能也很重要。Rust 并没有很多其他语言中有的空值功能。**空值**( *Null* )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
@ -220,59 +139,47 @@ Tony Hoare, null 的发明者,在他 2009 年的演讲 “Null References: Th
问题不在于概念而在于具体的实现。为此, Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 `Option<T>` ,而且它[定义于标准库中][option]<!-- ignore --> ,如下:
[option]: https://doc.rust-lang.org/std/option/enum.Option.html
```rust
enum Option< T > {
Some(T),
None,
Some(T),
}
```
`Option<T>` 枚举是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要 `Option::` 前缀来直接使用 `Some` 和 `None` 。即便如此 `Option<T>` 也仍是常规的枚举,`Some(T)` 和 `None` 仍是 `Option<T>` 的成员。
`<T>` 语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,第十章会更详细的讲解泛型。目前,所有你需要知道的就是 `<T>` 意味着 `Option` 枚举的 `Some` 成员可以包含任意类型的数据。这里是一些包含数字类型和字符串类型 `Option` 值的例子:
`<T>` 语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,第十章会更详细的讲解泛型。目前,所有你需要知道的就是 `<T>` 意味着 `Option` 枚举的 `Some` 成员可以包含任意类型的数据,同时每一个用于 `T` 位置的具体类型使得 `Option<T>` 整体作为不同的类型 。这里是一些包含数字类型和字符串类型 `Option` 值的例子:
```rust
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option< i32 > = None;
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-06-option-examples/src/main.rs:here}}
```
如果使用 `None` 而不是 `Some` ,需要告诉 Rust `Option<T>` 是什么类型的 ,因为编译器只通过 `None` 值无法推断出 `Some` 成员保存的值的类型。
`some_number` 的类型是 `Option<i32>` 。`some_string` 的类型是 `Option<&str>` ,这(与 `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` 相加:
```rust,ignore
let x: i8 = 5;
let y: Option< i8 > = Some(5);
let sum = x + y;
```rust,ignore,does_not_compile
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-07-cant-use-option-directly/src/main.rs:here}}
```
如果运行这些代码,将得到类似这样的错误信息:
```text
error[E0277]: the trait bound `i8: std::ops::Add<std::option::Option<i8>>` is
not satisfied
-->
|
5 | let sum = x + y;
| ^ no implementation for `i8 + std::option::Option<i8>`
|
```console
{{#include ../listings/ch06-enums-and-pattern-matching/no-listing-07-cant-use-option-directly/output.txt}}
```
很好!事实上,错误信息意味着 Rust 不知道该如何将 `Option<i8>` 与 `i8` 相加,因为它们的类型不同。当在 Rust 中拥有一个像 `i8` 这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。只有当使用 `Option<i8>` (或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。
换句话说,在对 `Option<T>` 进行 `T` 的运算之前必须将其转换为 `T` 。通常这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。
不再担心会错误的假设一个非空值 ,会让你对代码更加有信心。为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 `Option<T>` 中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 `Option<T>` 类型,你就 ** 可以** 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。
消除了错误地假设一个非空值的风险 ,会让你对代码更加有信心。为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 `Option<T>` 中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 `Option<T>` 类型,你就 ** 可以** 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。
那么当有一个 `Option<T>` 的值时,如何从 `Some` 成员中取出 `T` 的值来使用它呢?`Option< T > ` 枚举拥有大量用于各种情况的方法:你可以查看[它的文档][docs]<!-- ignore --> 。熟悉 `Option<T>` 的方法将对你的 Rust 之旅非常有用。
[docs]: https://doc.rust-lang.org/std/option/enum.Option.html
总的来说,为了使用 `Option<T>` 值,需要编写处理每个成员的代码。你想要一些代码只当拥有 `Some(T)` 值时运行,允许这些代码使用其中的 `T` 。也希望一些代码在值为 `None` 时运行,这些代码并没有一个可用的 `T` 值。`match` 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
[IpAddr]: https://doc.rust-lang.org/std/net/enum.IpAddr.html
[option]: https://doc.rust-lang.org/std/option/enum.Option.html
[docs]: https://doc.rust-lang.org/std/option/enum.Option.html