开发环境
- Windows 10
- Rust 1.58.1

- VS Code 1.64.2

项目工程
这里继续沿用上次工程rust-demo
枚举和模式匹配
枚举允许您通过枚举类型的可能变体来定义类型。首先,我们将定义并使用一个枚举来展示一个枚举如何编码数据的含义。接下来,我们将探索一个特别有用的枚举,称为Option,它表示一个值可以是某个值,也可以是零。然后,我们将看看match表达式中的模式匹配是如何简化为枚举的不同值运行不同代码的。最后,我们将介绍if let构造如何成为另一种方便、简洁的习惯用法,让您在代码中处理枚举。
枚举是许多语言中的一个特性,但是它们的功能在每种语言中都有所不同。
定义枚举
让我们来看一个我们可能想要用代码表达的情况,看看为什么在这种情况下枚举是有用的并且比结构更合适。假设我们需要处理IP地址。目前,IP地址使用两种主要标准:第四版和第六版。这些是我们的程序将遇到的IP地址的唯一可能性:我们可以枚举所有可能的变体,这就是枚举的名称。
任何IP地址都可以是版本四或版本六地址,但不能同时是两者。IP地址的该属性使得enum数据结构是合适的,因为enum值只能是其变体之一。第四版和第六版地址基本上都是IP地址,所以当代码处理适用于任何类型IP地址的情况时,它们应该被视为相同的类型。
我们可以通过定义IpAddrKind枚举并列出IP地址的可能类型(V4和V6)来用代码表达这个概念。这些是枚举的变体,如下所示:
// 定义枚举
enum IpAddrKind {
V4,
V6,
}
IpAddrKind现在是一个自定义数据类型,我们可以在代码中的其他地方使用它。
枚举值
我们可以创建IpAddrKind的两种变体的实例,如下所示:
// 使用枚举的值
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
请注意,枚举的变体是在其标识符下命名的,我们使用一个双冒号来分隔两者。这很有用的原因是,现在值IpAddrKind::V4和IpAddrKind::V6属于同一类型:IpAddrKind。
例如,我们可以定义一个接受任何IpAddrKind的函数:
fn route(ip_kind: IpAddrKind) {}
我们可以用任何一个变量来调用这个函数:
// 调用
route(IpAddrKind::V4);
route(IpAddrKind::V6);
使用枚举有更多的优点。多想想我们的IP地址类型,目前我们没有办法存储实际的IP地址数据;我们只知道是哪一种。刚刚学习了前面的结构体,可以定义如下所示的代码解决一些问题。
// 枚举
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,它有两个字段:一个是IpAddrKind类型的kind字段(我们之前定义的enum ),一个是String类型的address字段。我们有这个结构的两个实例。第一个是home,其类型的值为IpAddrKind::V4,关联的地址数据为127.0.0.1。第二个实例loopback使用IpAddrKind的另一个变体作为它的种类值V6,并有一个与之相关联的地址 ::1。我们已经使用了一个结构将kind和address值捆绑在一起,所以现在变量与值相关联。
通过将数据直接放入每个枚举变量中,我们可以用一种更简洁的方式表示相同的概念,只使用一个枚举,而不是一个结构中的枚举。IpAddr枚举的这个新定义表明,V4和V6变体都将具有关联的字符串值,如下所示:
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类型实例的函数调用。作为定义枚举的结果,我们自动得到这个构造函数。
使用enum而不是struct还有另一个好处:每个变量可以有不同类型和数量的关联数据。第四版类型IP地址将始终有四个数值部分,其值介于0和255之间。如果我们希望将V4地址存储为四个u8值,但仍然将V6地址表示为一个string值,我们将无法使用struct。枚举的方式如下:
// 定义枚举
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地址的几种不同方法。让我们看看标准库是如何定义IpAddr的:它拥有我们已经定义和使用的确切的enum和变量,但它以两种不同结构的形式将地址数据嵌入变量中,这两种结构针对每个变量进行了不同的定义,如下:
// 标准库中的IP4
struct Ipv4Addr {
// --snip--
}
// 标准库中的IP6
struct Ipv6Addr {
// --snip--
}
// 标准库中的IP地址枚举
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
这段代码说明了可以将任何类型的数据放入枚举变量中:例如,字符串、数值类型或结构。您甚至可以包含另一个枚举!此外,标准库类型通常并不比您可能想到的复杂多少。
请注意,即使标准库包含IpAddr的定义,我们仍然可以创建和使用自己的定义,而不会产生冲突,因为我们没有将标准库的定义纳入我们的范围。
我们看看另外一个例子:
// 定义枚举变量Message
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
该枚举有四种不同类型的变体:
- Quit根本没有与之相关的数据。
- Move像struct一样具有命名字段。
- Write包括单个String。
- ChangeColor包括三个i32值。
上例中的变量定义一个enum类似于定义不同种类的struct定义,除了enum不使用struct关键字,所有的变量都被分组在Message类型下。下列结构可以保存与前面的枚举变量相同的数据:
struct QuitMessage; // 结构体单元
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体
但是,如果我们使用不同的结构体,每个结构体都有自己的类型,我们就不能像使用上例中定义的单一类型的消息枚举那样容易地定义一个函数来接受这些类型的消息。
枚举和结构体之间还有一个相似之处:正如我们能够使用impl在结构上定义方法一样,我们也能够在枚举上定义方法,如下示例:
// 类似于结构体实现方法
impl Message {
fn call(&self) {
// 函数体
}
}
let m = Message::Write(String::from("hello"));
m.call();
该方法的主体将使用self来获取我们调用该方法的值。在本例中,我们创建了一个值为Message::Write(String::from(" hello "))的变量m,这就是当m.call()运行时self将出现在call方法体中的内容。
可以看看标准库中另一个非常常见和有用的枚举:Option。
Option枚举
在上一节中,我们看了IpAddr 枚举如何让我们使用Rust的类型系统将更多的信息而不仅仅是数据编码到我们的程序中。本节探索了Option的一个案例研究,这是由标准库定义的另一个枚举。Option类型被用在很多地方,因为它编码了一个非常常见的场景,在这个场景中,一个值可以是某个值,也可以是空值。用类型系统来表达这个概念意味着编译器可以检查你是否已经处理了你应该处理的所有情况;这种功能可以防止在其他编程语言中极其常见的错误。
编程语言设计通常被认为是根据你包括了哪些特性,但是你排除的特性也很重要。Rust没有许多其他语言都有的null特性。Null是一个值,表示那里没有值。在有null语言中,变量总是处于两种状态之一:null或非null。
null值的问题是,如果您试图将空值用作非空值,您会得到某种错误。因为这种null或not-null属性非常普遍,所以很容易出现这种错误。
然而,null试图表达的概念仍然是有用的:null是当前由于某种原因无效或不存在的值。
问题不在于概念,而在于具体的实现。因此,Rust没有nulls,但是它有一个enum,可以对值的存在或不存在的概念进行编码。此枚举就是Option<T>。
// Option的定义
enum Option<T> {
None,
Some(T),
}
Option< T> 枚举非常有用,它甚至包含在引子中;您不需要将其明确纳入范围。此外,它的变体也是如此:可以不使用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_string = Some("a string");
let absent_number: Option<i32> = None;
some_number的类型是Option<i32 >。some_string的类型是Option<&str >,这是一种不同的类型。Rust可以推断出这些类型,因为我们已经在Some中指定了。而absent_number,Rust要求我们注释整个选项类型:编译器无法通过只查看None值来推断相应的Some变量将包含的类型。这里,我们告诉Rust,我们的意思是让absent_number的类型为Option<i32 >。
当我们有一个Some值时,我们知道一个值存在,并且这个值保存在Some中。当我们有一个None值时,在某种意义上,它和null的意思是一样的:我们没有一个有效值。
简而言之,因为Option<T >和T(其中T可以是任何类型)是不同的类型,编译器不会让我们使用Option<T >值,就好像它肯定是一个有效值一样。例如:
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
println!("sum is {}", sum)
}
编译运行
cargo run
错误信息:

激烈!实际上,这个错误消息意味着Rust不知道如何添加i8和Option<i8 >,因为它们是不同的类型。当我们在Rust中有一个类似i8类型的值时,编译器将确保我们总是有一个有效值。 我们可以放心地继续,而不必在使用该值之前检查null。只有当我们有一个Option<i8 >(或者我们正在处理的任何类型的值)时,我们才需要担心可能没有值,编译器会确保我们在使用值之前处理了这种情况。
换句话说,您必须先将Option<T >转换为T,然后才能使用它执行T操作。一般来说,这有助于抓住null最常见的问题之一:假设某个东西实际上不为空。
不必担心错误地假定非空值,这有助于您对代码更有信心。为了获得一个可能为空的值,您必须通过将该值的类型显式的声明为Option<T >。然后,当您使用该值时,您需要显式地处理该值为空的情况。只要值的类型不是Option<T >,您就可以放心地假设该值不为空。这是Rust经过深思熟虑的设计决定,以限制空值的普遍性,并增加Rust代码的安全性。
那么,当你有一个Option<T >类型的值时,如何从某个变量中得到T值,这样你就可以使用这个值了?Option<T> 枚举提供了大量在各种情况下都有用的方法。熟悉Option<T >上的方法对您的Rust之旅非常有用。
一般来说,为了使用Option<T >值,您希望拥有处理每个变量的代码。您希望某些代码只在您有一个Some(T)值时运行,并且这些代码允许使用内部的T。如果你有一个None值,你想让其他代码运行,而那个代码没有T值可用。match表达式是一个控制流构造,当与枚举一起使用时,它可以做到这一点:它将运行不同的代码,这取决于它拥有的枚举的变体,并且该代码可以使用匹配值中的数据。
本章重点
- 枚举Enum的含义
- 定义枚举
- Option<T>枚举
409



被折叠的 条评论
为什么被折叠?



