「几种可能之一」怎么表达?
上一章的结构体表达「一个东西由哪些部分组成」。还有一种同样常见的形状:「一个东西是几种可能之一」。比如一次网络请求的结果:要么成功拿到网页,要么超时,要么被服务器拒绝。注意这三种可能各自带的数据还不一样——成功带一串 HTML,被拒带一个状态码,超时什么都不带。这种形状在 Rust 里的表达是枚举(enum):
enum FetchResult {
Success(String), // 带一串 HTML
Rejected(u16), // 带一个状态码
Timeout, // 什么都不带
}
别被其他语言里「枚举就是给数字起名字」的印象带偏。Rust 的枚举是带数据的标签:每个变体(variant)可以各自挂载不同类型、不同数量的数据。一个 FetchResult 值在任一时刻必然是三者之一,不多不少——编译器担保。
match:把每一种可能都摆到台面上
拿到一个枚举值后怎么拆包?用 match:
fn report(r: FetchResult) {
match r {
FetchResult::Success(html) => println!("拿到 {} 字节", html.len()),
FetchResult::Rejected(code) => println!("被拒了,状态码 {}", code),
FetchResult::Timeout => println!("超时了"),
}
}
match 逐条比对变体,命中哪条执行哪条,变体里挂的数据就地拆出来用。真正厉害的是这条铁律:match 必须穷尽所有可能。漏写一个变体,编译不过。将来你给枚举加了第四种可能,所有拆包它的 match 全部报错,逼着你挨个处理——重构从此有了保镖。
类比:别的语言处理「几种可能之一」像查电表,漏看一格就漏收一户的钱,而且没人提醒你;Rust 的 match 像出卷老师发答题卡——格子都印好了,漏填任何一格,当场交不了卷。
有时候你只关心一种可能,写全 match 太隆重,可以用 if let:
if let FetchResult::Success(html) = r {
println!("成功!{}", html.len());
} // 其他可能不管了
读作「如果 r 恰好是 Success 这种,就把 html 拆出来执行这段」。便利的代价是放弃了穷尽检查——只在你真的不在乎其它分支时用。
Option:把「可能没有」写进类型系统
现在讲枚举最重要的应用,也是 Rust 对整个编程世界的回答。先看一段每个程序员都写过的灾难:某语言里取用户头像,返回 null 表示「没有头像」,然后某处忘了判空——程序炸了。null 的发明者 Tony Hoare 自己管它叫「十亿美元的错误」:问题不在于 null 这个概念,而在于「可能是 null」这件事在类型上看不出来,编译器无从检查你判没判。
Rust 的标准库里没有 null。取而代之的是枚举 Option:
enum Option<T> {
Some(T), // 有,里面装着值
None, // 没有
}
(<T> 是泛型参数,表示「里面装什么类型都行」,第十一章细讲。)一个函数可能找不到结果,它的返回类型就写 Option<User>,而不是「返回 User 但可能偷偷是 null」。区别在哪?你想拿到里面的 User,必须经过一道拆包——None 的情况不处理,你就碰不到值:
let maybe = find_user(42); // 类型是 Option<User>,有没有一看签名就知道
match maybe {
Some(u) => println!("找到了:{}", u.name),
None => println!("查无此人"),
}
null 像一颗混进糖罐的图钉——看起来是糖,咬下去才知道;Option 像一个贴着「可能为空」警示标签的盒子——你必须开盒验货才能吃到糖。「十亿美元的错误」在 Rust 里的修复方案就这么朴素:把可能性从「暗坑」变成「明牌」。
Option 还配了一组顺手的工具方法:.unwrap_or(默认值) 给个兜底,.map(f) 有值就加工一下,.is_some() 只问有没有。日常代码里它们比手写 match 更常用,但骨子里都是同一个枚举。
「可能没有」有了明牌表达,下一个问题顺理成章:「可能失败」呢?读取文件可能失败、网络请求可能失败——这类「失败」和「没有」不一样,失败通常带着原因。下一章的 Result 和那个问号运算符,是 Rust 日常代码里出场率最高的搭档。