struct 把相关字段放在同一个值里;enum 列出一个值可能采用的不同形式,每一种形式称为 variant。IP 地址可以是 IPv4 或 IPv6,就适合从这里理解 enum。
variant 可以携带不同的数据
#[derive(Debug)]enum IpAddr { V4(u8, u8, u8, u8), V6(String),}
fn show(ip: IpAddr) { match ip { IpAddr::V4(a, b, c, d) => println!("{a}.{b}.{c}.{d}"), IpAddr::V6(text) => println!("{text}"), }}
fn main() { show(IpAddr::V4(127, 0, 0, 1)); show(IpAddr::V6(String::from("::1")));}两种值都属于 IpAddr 类型,因此能传给同一个 show 函数。这个教学类型用 String 保存 IPv6 文本,并不验证它是否为合法地址;实际处理地址可以使用标准库 std::net::IpAddr,其两个 variant 分别包含 Ipv4Addr 和 Ipv6Addr。
这里的 tuple-like variant 可以像函数一样构造,例如 IpAddr::V6(String::from("::1"))。不要把“每个 variant 都是构造函数”推广到所有形式:unit-like variant 直接写名字,struct-like variant 使用花括号。
用 Message 对照三种形式
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),}
impl Message { fn call(&self) { match self { Self::Quit => println!("quit"), Self::Move { x, y } => println!("move to {x}, {y}"), Self::Write(text) => println!("{text}"), Self::ChangeColor(r, g, b) => println!("color {r}, {g}, {b}"), } }}
fn main() { let messages = [ Message::Quit, Message::Move { x: 3, y: 4 }, Message::Write(String::from("hello")), Message::ChangeColor(255, 0, 0), ]; for message in &messages { message.call(); }}Quit 不带数据,Move 带命名字段,Write 和 ChangeColor 带位置字段。call(&self) 借用消息,match self 中的模式按照引用匹配,不会把 Write 里的 String move 出来。
Option:明确表示可能没有值
标准库 Option<T> 有 Some(T) 和 None 两个 variant,并与它们一起进入 prelude,可以直接使用。
flowchart TD A[Option 的一个值] --> B{匹配 variant} B --> C[Some: 含一个 T] B --> D[None: 没有 T] C --> E[使用内部值] D --> F[返回默认值或执行其他分支]Option<i32> 与 i32 是不同类型。前者可能为空,不能直接与整数相加:
fn main() { let value: Option<i32> = Some(5); let sum = 5 + value; println!("{sum}");}这段预期编译失败,错误 E0277。可以先匹配 Some 和 None。Option 帮助表达缺失,但使用 unwrap() 处理 None 仍会 panic;Rust 也存在可以为空的 raw pointer,不能把它概括成“语言中没有任何空指针”。
match:覆盖所有可能情况
下面沿用 Book 的“可选整数加一”例子,测试的值没有发生溢出:
fn plus_one(value: Option<i32>) -> Option<i32> { match value { Some(number) => Some(number + 1), None => None, }}
fn main() { assert_eq!(plus_one(Some(5)), Some(6)); assert_eq!(plus_one(None), None);}Some(number) 把内部值绑定给 number。match 按顺序选择第一个匹配的 arm,并产生该 arm 的结果;各 arm 的结果要能统一为兼容类型。这个加一函数仍受 i32 范围限制;需要处理最大值时,可在 Some 分支返回 number.checked_add(1)。
漏掉 None 的反例预期编译失败,错误 E0004:
fn main() { let value = Some(5); match value { Some(number) => println!("{number}"), }}other 与 _
fn main() { let dice_roll = 9; match dice_roll { 3 => println!("add hat"), 7 => println!("remove hat"), other => println!("move {other} steps"), } match dice_roll { 3 | 7 => println!("hat event"), _ => (), }}other 是变量名,匹配并绑定剩余值;_ 只匹配,不绑定。通配 arm 一般放最后,否则可能先匹配掉后面希望处理的情况。
if let:只处理关心的模式
fn main() { let config_max = Some(3u8); if let Some(max) = config_max { println!("maximum={max}"); }}匹配 Some(max) 时执行代码,否则跳过。if let 不要求列出每个 variant,适合只关心一个模式的场景;如果每种情况都需要处理,用 match 更容易核对覆盖范围。
let…else:不匹配就提前退出
fn describe_limit(limit: Option<u8>) -> String { let Some(max) = limit else { return String::from("no limit"); }; format!("limit={max}")}
fn main() { assert_eq!(describe_limit(Some(3)), "limit=3"); assert_eq!(describe_limit(None), "no limit");}匹配成功后,max 可以在后续 scope 使用。else 必须 diverge,不能正常继续到后面使用 max 的代码;return、退出相应循环的 break / continue、panic! 都可以用于合适的上下文。整个 let...else 语句末尾需要分号。
参考:Defining an Enum、The match Control Flow Construct、if let and let…else。
评论