Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

定义枚举

Defining an Enum

结构体让你能够将相关的字段和数据分组,比如具有 widthheightRectangle;而枚举则让你能够表达一个值是某一组可能值中的一个。例如,我们可能想说 Rectangle 是可能形状集合中的一个,该集合还包括 Circle(圆形)和 Triangle(三角形)。为此,Rust 允许我们将这些可能性编码为一个枚举。

Where structs give you a way of grouping together related fields and data, like a Rectangle with its width and height, enums give you a way of saying a value is one of a possible set of values. For example, we may want to say that Rectangle is one of a set of possible shapes that also includes Circle and Triangle. To do this, Rust allows us to encode these possibilities as an enum.

让我们看一个我们可能想在代码中表达的情况,看看为什么在这种情况下枚举比结构体更有用且更合适。假设我们需要处理 IP 地址。目前,IP 地址有两种主要标准:版本四和版本六。因为我们的程序只会遇到这两种 IP 地址的可能性,所以我们可以“枚举”所有可能的变体,这就是“枚举”名称的由来。

Let’s look at a situation we might want to express in code and see why enums are useful and more appropriate than structs in this case. Say we need to work with IP addresses. Currently, two major standards are used for IP addresses: version four and version six. Because these are the only possibilities for an IP address that our program will come across, we can enumerate all possible variants, which is where enumeration gets its name.

任何 IP 地址要么是版本四地址,要么是版本六地址,但不能同时是两者。IP 地址的这种属性使得枚举数据结构非常合适,因为枚举值只能是其变体之一。版本四和版本六地址从根本上说仍然都是 IP 地址,因此当代码处理适用于任何类型 IP 地址的情况时,它们应该被视为相同的类型。

Any IP address can be either a version four or a version six address, but not both at the same time. That property of IP addresses makes the enum data structure appropriate because an enum value can only be one of its variants. Both version four and version six addresses are still fundamentally IP addresses, so they should be treated as the same type when the code is handling situations that apply to any kind of IP address.

我们可以通过定义 IpAddrKind 枚举并列出 IP 地址可能的类型 V4V6 来在代码中表达这个概念。这些是枚举的变体:

We can express this concept in code by defining an IpAddrKind enumeration and listing the possible kinds an IP address can be, V4 and V6. These are the variants of the enum:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

IpAddrKind 现在是一个自定义数据类型,我们可以在代码的其他地方使用它。

IpAddrKind is now a custom data type that we can use elsewhere in our code.

枚举值

Enum Values

我们可以像这样创建 IpAddrKind 两个变体的每一个实例:

We can create instances of each of the two variants of IpAddrKind like this:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

注意,枚举的变体命名空间在其标识符之下,我们使用双冒号来分隔两者。这很有用,因为现在 IpAddrKind::V4IpAddrKind::V6 这两个值都属于同一个类型:IpAddrKind。例如,我们可以定义一个接受任何 IpAddrKind 的函数:

Note that the variants of the enum are namespaced under its identifier, and we use a double colon to separate the two. This is useful because now both values IpAddrKind::V4 and IpAddrKind::V6 are of the same type: IpAddrKind. We can then, for instance, define a function that takes any IpAddrKind:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

我们可以使用任何变体来调用此函数:

And we can call this function with either variant:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

使用枚举还有更多优势。再深入思考我们的 IP 地址类型,目前我们还没有办法存储实际的 IP 地址“数据”;我们只知道它是哪种“类型”。鉴于你刚刚在第 5 章中学习了结构体,你可能会想用结构体来解决这个问题,如示例 6-1 所示。

Using enums has even more advantages. Thinking more about our IP address type, at the moment we don’t have a way to store the actual IP address data; we only know what kind it is. Given that you just learned about structs in Chapter 5, you might be tempted to tackle this problem with structs as shown in Listing 6-1.

fn main() {
    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"),
    };
}

在这里,我们定义了一个结构体 IpAddr,它有两个字段:一个是 kind 字段,其类型为 IpAddrKind(我们之前定义的枚举);另一个是 address 字段,类型为 String。我们有两个此结构体的实例。第一个是 home,它的 kind 值为 IpAddrKind::V4,关联的地址数据为 127.0.0.1。第二个实例是 loopback。它的 kind 值为 IpAddrKind::V6,关联的地址为 ::1。我们使用结构体将 kindaddress 值捆绑在一起,因此现在变体与值关联起来了。

Here, we’ve defined a struct IpAddr that has two fields: a kind field that is of type IpAddrKind (the enum we defined previously) and an address field of type String. We have two instances of this struct. The first is home, and it has the value IpAddrKind::V4 as its kind with associated address data of 127.0.0.1. The second instance is loopback. It has the other variant of IpAddrKind as its kind value, V6, and has address ::1 associated with it. We’ve used a struct to bundle the kind and address values together, so now the variant is associated with the value.

然而,仅使用枚举来表示相同的概念更为简洁:我们不使用结构体内部的枚举,而是直接将数据放入每个枚举变体中。IpAddr 枚举的新定义表明 V4V6 变体都将具有关联的 String 值:

However, representing the same concept using just an enum is more concise: Rather than an enum inside a struct, we can put data directly into each enum variant. This new definition of the IpAddr enum says that both V4 and V6 variants will have associated String values:

fn main() {
    enum IpAddr {
        V4(String),
        V6(String),
    }

    let home = IpAddr::V4(String::from("127.0.0.1"));

    let loopback = IpAddr::V6(String::from("::1"));
}

我们直接将数据附加到枚举的每个变体,因此不需要额外的结构体。在这里,也更容易看到枚举工作的另一个细节:我们定义的每个枚举变体名称也变成了一个构造枚举实例的函数。也就是说,IpAddr::V4() 是一个函数调用,它接收一个 String 参数并返回 IpAddr 类型的一个实例。由于定义了枚举,我们自动获得了这个定义的构造函数。

We attach data to each variant of the enum directly, so there is no need for an extra struct. Here, it’s also easier to see another detail of how enums work: The name of each enum variant that we define also becomes a function that constructs an instance of the enum. That is, IpAddr::V4() is a function call that takes a String argument and returns an instance of the IpAddr type. We automatically get this constructor function defined as a result of defining the enum.

使用枚举而不是结构体还有另一个优势:每个变体可以具有不同类型和数量的关联数据。版本四 IP 地址总是有四个数值组成部分,其值在 0 到 255 之间。如果我们想将 V4 地址存储为四个 u8 值,但仍将 V6 地址表示为一个 String 值,使用结构体就无法做到。枚举可以轻松处理这种情况:

There’s another advantage to using an enum rather than a struct: Each variant can have different types and amounts of associated data. Version four IP addresses will always have four numeric components that will have values between 0 and 255. If we wanted to store V4 addresses as four u8 values but still express V6 addresses as one String value, we wouldn’t be able to with a struct. Enums handle this case with ease:

fn main() {
    enum IpAddr {
        V4(u8, u8, u8, u8),
        V6(String),
    }

    let home = IpAddr::V4(127, 0, 0, 1);

    let loopback = IpAddr::V6(String::from("::1"));
}

我们展示了几种定义数据结构来存储版本四和版本六 IP 地址的不同方法。然而,事实证明,想要存储 IP 地址并编码它们的类型是非常普遍的需求,以至于标准库中已经有一个我们可以使用的定义! 让我们看看标准库是如何定义 IpAddr 的。它具有我们定义并使用的完全相同的枚举和变体,但它以两个不同结构体的形式将地址数据嵌入到变体内部,这两个结构体针对每个变体进行了不同的定义:

We’ve shown several different ways to define data structures to store version four and version six IP addresses. However, as it turns out, wanting to store IP addresses and encode which kind they are is so common that the standard library has a definition we can use! Let’s look at how the standard library defines IpAddr. It has the exact enum and variants that we’ve defined and used, but it embeds the address data inside the variants in the form of two different structs, which are defined differently for each variant:

#![allow(unused)]
fn main() {
struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}
}

这段代码说明你可以将任何类型的数据放入枚举变体中:例如字符串、数值类型或结构体。你甚至可以包含另一个枚举!此外,标准库类型的复杂程度通常不会比你自己想出的复杂多少。

This code illustrates that you can put any kind of data inside an enum variant: strings, numeric types, or structs, for example. You can even include another enum! Also, standard library types are often not much more complicated than what you might come up with.

注意,即使标准库包含 IpAddr 的定义,我们仍然可以创建并使用我们自己的定义而不会发生冲突,因为我们还没有将标准库的定义引入我们的作用域。我们将在第 7 章更多地讨论将类型引入作用域。

Note that even though the standard library contains a definition for IpAddr, we can still create and use our own definition without conflict because we haven’t brought the standard library’s definition into our scope. We’ll talk more about bringing types into scope in Chapter 7.

让我们看看示例 6-2 中的另一个枚举例子:这个枚举在其变体中嵌入了各种各样的类型。

Let’s look at another example of an enum in Listing 6-2: This one has a wide variety of types embedded in its variants.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}

这个枚举有四个不同类型的变体:

This enum has four variants with different types:

  • Quit:完全没有关联数据。

  • Quit: Has no data associated with it at all

  • Move:具有命名字段,就像结构体一样。

  • Move: Has named fields, like a struct does

  • Write:包含一个 String

  • Write: Includes a single String

  • ChangeColor:包含三个 i32 值。

  • ChangeColor: Includes three i32 values

定义一个具有像示例 6-2 这种变体的枚举,类似于定义不同种类的结构体,不同之处在于枚举不使用 struct 关键字,并且所有变体都归组在 Message 类型下。以下结构体可以持有与前述枚举变体相同的数据:

Defining an enum with variants such as the ones in Listing 6-2 is similar to defining different kinds of struct definitions, except the enum doesn’t use the struct keyword and all the variants are grouped together under the Message type. The following structs could hold the same data that the preceding enum variants hold:

struct QuitMessage; // unit struct
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct

fn main() {}

但是,如果我们使用这些不同的结构体(每个结构体都有自己的类型),我们就无法像使用示例 6-2 中定义的单一类型的 Message 枚举那样,轻易地定义一个接受这些任何一种消息的函数。

But if we used the different structs, each of which has its own type, we couldn’t as easily define a function to take any of these kinds of messages as we could with the Message enum defined in Listing 6-2, which is a single type.

枚举和结构体之间还有一个相似之处:就像我们可以使用 impl 在结构体上定义方法一样,我们也能够在枚举上定义方法。这是一个我们可以在 Message 枚举上定义的名为 call 的方法:

There is one more similarity between enums and structs: Just as we’re able to define methods on structs using impl, we’re also able to define methods on enums. Here’s a method named call that we could define on our Message enum:

fn main() {
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }

    impl Message {
        fn call(&self) {
            // method body would be defined here
        }
    }

    let m = Message::Write(String::from("hello"));
    m.call();
}

方法体将使用 self 来获取我们调用方法的那个值。在这个例子中,我们创建了一个变量 m,它的值为 Message::Write(String::from("hello")),这就是当 m.call() 运行时 call 方法体中的 self

The body of the method would use self to get the value that we called the method on. In this example, we’ve created a variable m that has the value Message::Write(String::from("hello")), and that is what self will be in the body of the call method when m.call() runs.

让我们看看标准库中另一个非常常见且有用的枚举:Option

Let’s look at another enum in the standard library that is very common and useful: Option.

Option 枚举

The Option Enum

本节将探讨 Option 的案例研究,它是标准库定义的另一个枚举。Option 类型编码了一个非常常见的场景,即一个值可能存在,也可能不存在。

This section explores a case study of Option, which is another enum defined by the standard library. The Option type encodes the very common scenario in which a value could be something, or it could be nothing.

例如,如果你请求一个非空列表中的第一个项目,你会得到一个值。如果你请求一个空列表中的第一个项目,你会一无所获。从类型系统的角度表达这个概念意味着编译器可以检查你是否处理了你应该处理的所有情况;这种功能可以防止其他编程语言中极其常见的错误。

For example, if you request the first item in a non-empty list, you would get a value. If you request the first item in an empty list, you would get nothing. Expressing this concept in terms of the type system means the compiler can check whether you’ve handled all the cases you should be handling; this functionality can prevent bugs that are extremely common in other programming languages.

编程语言设计通常考虑包含哪些功能,但排除哪些功能也很重要。Rust 没有许多其他语言所具有的空值(null)功能。空值Null)是一个表示那里没有值的值。在具有空值的语言中,变量始终处于两种状态之一:空或非空。

Programming language design is often thought of in terms of which features you include, but the features you exclude are important too. Rust doesn’t have the null feature that many other languages have. Null is a value that means there is no value there. In languages with null, variables can always be in one of two states: null or not-null.

托尼·霍尔(Tony Hoare),空值的发明者,在他 2009 年的演讲《空引用:价值十亿美元的错误》中这样说道:

In his 2009 presentation “Null References: The Billion Dollar Mistake,” Tony Hoare, the inventor of null, had this to say:

我称它为我价值十亿美元的错误。当时,我正在为一种面向对象语言设计第一个全面的引用类型系统。我的目标是确保所有引用的使用都绝对安全,由编译器自动执行检查。但我禁不住诱惑,加入了一个空引用,仅仅是因为它实现起来非常容易。这导致了无数的错误、漏洞和系统崩溃,在过去的四十年里可能造成了十亿美元的痛苦和损失。

I call it my billion-dollar mistake. At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

空值的问题在于,如果你尝试将空值作为非空值使用,你会得到某种类型的错误。由于这种空或非空的属性无处不在,极其容易犯这种错误。

The problem with null values is that if you try to use a null value as a not-null value, you’ll get an error of some kind. Because this null or not-null property is pervasive, it’s extremely easy to make this kind of error.

然而,空值试图表达的概念仍然是一个有用的概念:空值是一个当前由于某种原因而无效或缺失的值。

However, the concept that null is trying to express is still a useful one: A null is a value that is currently invalid or absent for some reason.

问题实际上不在于概念,而在于具体的实现。因此,Rust 没有空值,但它确实有一个可以编码值存在或缺失概念的枚举。这个枚举就是 Option<T>,它由标准库定义如下:

The problem isn’t really with the concept but with the particular implementation. As such, Rust does not have nulls, but it does have an enum that can encode the concept of a value being present or absent. This enum is Option<T>, and it is defined by the standard library as follows:

#![allow(unused)]
fn main() {
enum Option<T> {
    None,
    Some(T),
}
}

Option<T> 枚举非常有用,以至于它甚至被包含在预导入模块(prelude)中;你不需要显式地将其引入作用域。它的变体也包含在预导入模块中:你可以直接使用 SomeNone,而不需要 Option:: 前缀。Option<T> 枚举仍然只是一个普通枚举,而 Some(T)None 仍然是 Option<T> 类型的变体。

The Option<T> enum is so useful that it’s even included in the prelude; you don’t need to bring it into scope explicitly. Its variants are also included in the prelude: You can use Some and None directly without the Option:: prefix. The Option<T> enum is still just a regular enum, and Some(T) and None are still variants of type Option<T>.

<T> 语法是我们尚未谈论的 Rust 功能。它是一个泛型类型参数,我们将在第 10 章更详细地介绍泛型。目前,你只需要知道 <T> 意味着 Option 枚举的 Some 变体可以持有任何类型的一块数据,并且每个用来替代 T 的具体类型都会使整体 Option<T> 类型成为不同的类型。下面是一些使用 Option 值持有数值类型和字符类型的例子:

The <T> syntax is a feature of Rust we haven’t talked about yet. It’s a generic type parameter, and we’ll cover generics in more detail in Chapter 10. For now, all you need to know is that <T> means that the Some variant of the Option enum can hold one piece of data of any type, and that each concrete type that gets used in place of T makes the overall Option<T> type a different type. Here are some examples of using Option values to hold number types and char types:

fn main() {
    let some_number = Some(5);
    let some_char = Some('e');

    let absent_number: Option<i32> = None;
}

some_number 的类型是 Option<i32>some_char 的类型是 Option<char>,这是一个不同的类型。Rust 可以推断这些类型,因为我们在 Some 变体中指定了一个值。对于 absent_number,Rust 要求我们注解整体的 Option 类型:编译器无法仅通过观察 None 值来推断相应的 Some 变体将持有的类型。在这里,我们告诉 Rust 我们希望 absent_number 的类型为 Option<i32>

The type of some_number is Option<i32>. The type of some_char is Option<char>, which is a different type. Rust can infer these types because we’ve specified a value inside the Some variant. For absent_number, Rust requires us to annotate the overall Option type: The compiler can’t infer the type that the corresponding Some variant will hold by looking only at a None value. Here, we tell Rust that we mean for absent_number to be of type Option<i32>.

当我们有一个 Some 值时,我们知道值是存在的,并且值被保存在 Some 内部。当我们有一个 None 值时,从某种意义上说,它和空值的意思一样:我们没有一个有效的值。那么,为什么拥有 Option<T> 会比拥有空值更好呢?

When we have a Some value, we know that a value is present, and the value is held within the Some. When we have a None value, in some sense it means the same thing as null: We don’t have a valid value. So, why is having Option<T> any better than having null?

简而言之,因为 Option<T>T(其中 T 可以是任何类型)是不同的类型,编译器不允许我们将 Option<T> 值当作肯定是一个有效值来使用。例如,这段代码无法编译,因为它尝试将 i8 加到 Option<i8> 上:

In short, because Option<T> and T (where T can be any type) are different types, the compiler won’t let us use an Option<T> value as if it were definitely a valid value. For example, this code won’t compile, because it’s trying to add an i8 to an Option<i8>:

fn main() {
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y;
}

如果我们运行这段代码,会得到如下错误信息:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
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>`:
            `&i8` implements `Add<i8>`
            `&i8` implements `Add`
            `i8` implements `Add<&i8>`
            `i8` implements `Add`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error

语气强烈!实际上,这条错误信息意味着 Rust 不明白如何将 i8Option<i8> 相加,因为它们是不同的类型。当我们在 Rust 中拥有一个像 i8 这样类型的值时,编译器将确保我们始终拥有一个有效值。我们可以自信地继续,而不必在使用该值之前检查空值。只有当我们拥有一个 Option<i8>(或者我们正在处理的任何类型的值)时,我们才必须担心可能没有值,并且编译器将确保我们在使用该值之前处理了那种情况。

Intense! In effect, this error message means that Rust doesn’t understand how to add an i8 and an Option<i8>, because they’re different types. When we have a value of a type like i8 in Rust, the compiler will ensure that we always have a valid value. We can proceed confidently without having to check for null before using that value. Only when we have an Option<i8> (or whatever type of value we’re working with) do we have to worry about possibly not having a value, and the compiler will make sure we handle that case before using the value.

换句话说,在你对 Option<T> 执行 T 操作之前,你必须将其转换为 T。通常,这有助于捕捉空值最常见的问题之一:假设某物不是空值,而实际上它是空值。

In other words, you have to convert an Option<T> to a T before you can perform T operations with it. Generally, this helps catch one of the most common issues with null: assuming that something isn’t null when it actually is.

消除错误地假设非空值的风险有助于你对代码更有信心。为了拥有一个可能为空的值,你必须显式地通过将该值的类型设为 Option<T> 来选择加入。然后,当你使用该值时,你被要求显式地处理该值为空的情况。只要一个值的类型不是 Option<T>,你就可以安全地假设该值不是空值。这是 Rust 为限制空值的普遍性并提高 Rust 代码安全性而做出的深思熟虑的设计决定。

Eliminating the risk of incorrectly assuming a not-null value helps you be more confident in your code. In order to have a value that can possibly be null, you must explicitly opt in by making the type of that value Option<T>. Then, when you use that value, you are required to explicitly handle the case when the value is null. Everywhere that a value has a type that isn’t an Option<T>, you can safely assume that the value isn’t null. This was a deliberate design decision for Rust to limit null’s pervasiveness and increase the safety of Rust code.

那么,当你拥有一个 Option<T> 类型的值时,如何从 Some 变体中获取 T 值以便使用该值呢?Option<T> 枚举拥有大量在各种情况下都有用的方法;你可以在其文档中查看它们。熟悉 Option<T> 上的方法将在你的 Rust 旅程中极其有用。

So how do you get the T value out of a Some variant when you have a value of type Option<T> so that you can use that value? The Option<T> enum has a large number of methods that are useful in a variety of situations; you can check them out in its documentation. Becoming familiar with the methods on Option<T> will be extremely useful in your journey with Rust.

通常,为了使用 Option<T> 值,你希望拥有能够处理每个变体的代码。你希望一些代码仅在你拥有 Some(T) 值时运行,并且这些代码被允许使用内部的 T。你希望另一些代码仅在你拥有 None 值时运行,并且这些代码没有 T 值可用。match 表达式是一个控制流结构,当与枚举一起使用时,它正是这样做的:它将根据枚举的变体运行不同的代码,并且这些代码可以使用匹配值内部的数据。

In general, in order to use an Option<T> value, you want to have code that will handle each variant. You want some code that will run only when you have a Some(T) value, and this code is allowed to use the inner T. You want some other code to run only if you have a None value, and that code doesn’t have a T value available. The match expression is a control flow construct that does just this when used with enums: It will run different code depending on which variant of the enum it has, and that code can use the data inside the matching value.