枚举允许你通过列举可能的 成员(variants) 来定义一个类型。
定义枚举
enum IpAddrKind { V4, V6,}
枚举值
创建 IpAddrKind 两个不同成员的实例
let four = IpAddrKind::V4;let six = IpAddrKind::V6;
注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在 IpAddrKind::V4 和 IpAddrKind::V6 都是 IpAddrKind 类型的。例如,接着可以定义一个函数来获取任何 IpAddrKind:
fn route(ip_type: IpAddrKind) { }route(IpAddrKind::V4);route(IpAddrKind::V6);
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"),};// 简洁方式enum IpAddr { V4(String), V6(String),}let home = IpAddr::V4(String::from("127.0.0.1"));let loopback = IpAddr::V6(String::from("::1"));// 我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。// 同理enum IpAddr { V4(u8, u8, u8, u8), V6(String),}let home = IpAddr::V4(127, 0, 0, 1);let loopback = IpAddr::V6(String::from("::1"));
其他例子
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),}
- Quit 没有关联任何数据。
- Move 包含一个匿名结构体。
- Write 包含单独一个 String。
- ChangeColor 包含三个 i32。
struct QuitMessage; // 类单元结构体struct MoveMessage { x: i32, y: i32,}struct WriteMessage(String); // 元组结构体struct ChangeColorMessage(i32, i32, i32); // 元组结构体
如果我们使用不同的结构体,由于它们都有不同的类型,我们将不能像使用 Message 枚举那样,轻易的定义一个能够处理这些不同类型的结构体的函数,因为枚举是单独一个类型。
结构体和枚举还有另一个相似点:就像可以使用 impl 来为结构体定义方法那样,也可以在枚举上定义方法。
impl Message { fn call(&self) { // 在这里定义方法体 }}let m = Message::Write(String::from("hello"));m.call();
Option 枚举和其相对于空值的优势
Option 是标准库定义的一个枚举,即一个值要么有值要么没值。
Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。
enum Option<T> { Some(T), None,}
Option 枚举是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要 Option:: 前缀来直接使用 Some 和 None。即便如此 Option 也仍是常规的枚举,Some(T) 和 None 仍是 Option 的成员。
是一个泛型类型参数
let some_number = Some(5);let some_string = Some("a string");let absent_number: Option<i32> = None;// 如果使用 None 而不是 Some,需要告诉 Rust Option<T> 是什么类型的// 因为编译器只通过 None 值无法推断出 Some 成员保存的值的类型。
let x: i8 = 5;let y: Option<i8> = Some(5);let sum = x + y;// error// Option<i8> 与 i8 相加,因为它们的类型不同
在对 Option 进行 T 的运算之前必须将其转换为 T。通常这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。
只要一个值不是 Option 类型,你就 可以 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。
match 控制流运算符
Rust 有一个叫做 match 的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成;
enum Coin { Penny, Nickel, Dime, Quarter,}fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }}
match 关键字后跟一个表达式,有点类似if
不过这里有一个非常大的区别:对于 if,表达式必须返回一个布尔值,而这里它可以是任何类型的。
=> 运算符将模式和将要运行的代码分开,每一个分支之间使用逗号分隔。
当 match 表达式执行时,它将结果值按顺序与每一个分支的模式相比较。如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支,非常类似一个硬币分类器。
fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 }, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }}// 如果分支代码较短的话通常不使用大括号
绑定值的模式
#[derive(Debug)] // 这样可以可以立刻看到州的名称enum UsState { Alabama, Alaska, // --snip--}enum Coin { Penny, Nickel, Dime, Quarter(UsState),}fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {:?}!", state); 25 }, }}// state会绑定coin对应的UsState
匹配 Option
fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), }}let five = Some(5);let six = plus_one(five);let none = plus_one(None);
匹配 Some(T)
当调用 plus_one(five) 时,plus_one 函数体中的 x 将会是值 Some(5)。
Some(5) 与 Some(i) 匹配吗?当然匹配!它们是相同的成员。i 绑定了 Some 中包含的值,所以 i 的值是 5。接着匹配分支的代码被执行,所以我们将 i 的值加一并返回一个含有值 6 的新 Some。
match 一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这里的i就是
匹配是穷尽的
fn plus_one(x: Option<i32>) -> Option<i32> { match x { Some(i) => Some(i + 1), }}// 失败// 我们没有处理 None 的情况,所以这些代码会造成一个 bug。
Rust 中的匹配是 穷尽的(exhaustive):必须穷举到最后的可能性来使代码有效。
_ 通配符
Rust 也提供了一个模式用于不想列举出所有可能值的场景。
let some_u8_value = 0u8;match some_u8_value { 1 => println!("one"), 3 => println!("three"), 5 => println!("five"), 7 => println!("seven"), _ => (),}
模式会匹配所有的值。通过将其放置于其他分支之后, 将会匹配所有之前没有指定的可能的值。() 就是 unit 值,所以 _ 的情况什么也不会发生。
if let 简单控制流
if let 语法让我们以一种不那么冗长的方式结合 if 和 let,来处理只匹配一个模式的值而忽略其他模式的情况。
let some_u8_value = Some(0u8);match some_u8_value { Some(3) => println!("three"), _ => (),}// 等价if let Some(3) = some_u8_value { println!("three");}
使用 if let 意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去 match 强制要求的穷尽性检查。match 和 if let 之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。
可以认为 if let 是 match 的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。
可以在 if let 中包含一个 else。else 块中的代码与 match 表达式中的 _ 分支块中的代码相同,这样的 match 表达式就等同于 if let 和 else。
let mut count = 0;match coin { Coin::Quarter(state) => println!("State quarter from {:?}!", state), _ => count += 1,}let mut count = 0;if let Coin::Quarter(state) = coin { println!("State quarter from {:?}!", state);} else { count += 1;}
如果你的程序遇到一个使用 match 表达起来过于啰嗦的逻辑,记住 if let 也在你的 Rust 工具箱中。