《Rust 编程实战》系列第 21 篇
在上一篇文章中,我们学习了 HashMap。
在使用 HashMap 查询数据时,经常会看到这样的代码:
1map.get("Tom") 2
它的返回值并不是直接返回某个值,而是:
1Option<&V> 2
为什么?
因为指定的键可能存在,也可能不存在。
Rust 使用:
1Option<T> 2
来表示这种“可能有值,也可能没有值”的情况。
它是 Rust 中最重要的枚举类型之一。
本文将介绍:
- 什么是
Option<T> Some与None- 为什么 Rust 不依赖传统空值
match处理 Optionif letunwrap()与expect()- 默认值处理
map()、and_then()- 函数返回 Option
- 实战案例
- 常见错误与最佳实践
什么是 Option?
Option<T> 的定义可以简单理解为:
1enum Option<T> { 2 Some(T), 3 None, 4} 5
它只有两种状态。
有值:
1Some(value) 2
没有值:
1None 2
例如:
1fn main() { 2 let first = Some(100); 3 let second: Option<i32> = None; 4 5 println!("{:?}", first); 6 println!("{:?}", second); 7} 8
输出:
1Some(100) 2None 3
为什么需要 Option?
假设我们要查询一个用户年龄。
用户可能存在:
120 2
也可能不存在。
一些语言可能使用:
1null 2
表示没有值。
但如果忘记检查空值,就可能产生运行时错误。
Rust 更倾向于把“可能不存在”明确写进类型:
1Option<i32> 2
这意味着:
调用者必须意识到这个值有可能不存在。
Some 与 None
有值时使用:
1Some() 2
例如:
1let age = Some(20); 2
它的类型通常是:
1Option<i32> 2
没有值时:
1let age: Option<i32> = None; 2
这里通常需要明确类型,因为单独一个 None 无法告诉编译器内部应该是什么类型。
Option 与 T 不同
下面两个变量类型完全不同:
1let age: i32 = 20; 2 3let optional_age: Option<i32> = 4 Some(20); 5
不能直接这样相加:
1let result = age + optional_age; 2
因为:
1i32 2
和:
1Option<i32> 2
不是同一种类型。
必须先把 Option 中的值安全地取出来。
使用 match 处理 Option
最完整的处理方式是:
1match 2
例如:
1fn main() { 2 let score = Some(90); 3 4 match score { 5 Some(value) => { 6 println!("成绩:{}", value); 7 } 8 None => { 9 println!("没有成绩"); 10 } 11 } 12} 13
输出:
1成绩:90 2
这里必须处理:
1Some 2
和:
1None 2
两个分支。
函数返回 Option
假设我们实现一个查找函数:
1fn find_user(id: u64) 2 -> Option<&'static str> 3{ 4 match id { 5 1 => Some("Tom"), 6 2 => Some("Alice"), 7 _ => None, 8 } 9} 10 11fn main() { 12 match find_user(2) { 13 Some(name) => { 14 println!("用户:{}", name); 15 } 16 None => { 17 println!("用户不存在"); 18 } 19 } 20} 21
当用户存在时:
1Some("Alice") 2
不存在时:
1None 2
这种设计比返回一个特殊字符串:
1"不存在" 2
更加明确。
使用 if let 简化 Option
如果只关心 Some,可以使用:
1if let 2
1fn main() { 2 let username = Some("Tom"); 3 4 if let Some(name) = username { 5 println!("用户名:{}", name); 6 } 7} 8
相当于只处理:
1Some 2
而忽略 None。
如果还需要处理 None:
1if let Some(name) = username { 2 println!("用户名:{}", name); 3} else { 4 println!("用户名不存在"); 5} 6
使用 unwrap() 获取值
可以使用:
1unwrap() 2
直接获取内部值:
1fn main() { 2 let number = Some(100); 3 4 let value = number.unwrap(); 5 6 println!("{}", value); 7} 8
输出:
1100 2
但是如果:
1let number: Option<i32> = None; 2
再执行:
1number.unwrap(); 2
程序会 panic。
因此:
不确定一定有值时,不要随意使用 unwrap()。
使用 expect()
expect() 与 unwrap() 类似,但可以指定错误信息。
1fn main() { 2 let config = Some("127.0.0.1"); 3 4 let host = config.expect( 5 "服务器地址不能为空" 6 ); 7 8 println!("{}", host); 9} 10
如果为 None,错误信息会更加明确。
在测试代码或程序启动阶段,如果某个值理论上必须存在,expect() 通常比 unwrap() 更容易排查问题。
使用 unwrap_or() 设置默认值
如果值不存在,可以提供默认值:
1fn main() { 2 let score: Option<i32> = None; 3 4 let value = score.unwrap_or(0); 5 6 println!("{}", value); 7} 8
输出:
10 2
如果:
1let score = Some(90); 2
则得到:
190 2
unwrap_or_else()
如果默认值需要通过计算获得,可以使用:
1unwrap_or_else() 2
1fn default_name() -> String { 2 String::from("游客") 3} 4 5fn main() { 6 let username: Option<String> = None; 7 8 let name = username 9 .unwrap_or_else(default_name); 10 11 println!("{}", name); 12} 13
只有在 None 时,才会调用默认值函数。
使用 map() 转换 Option
假设:
1let value = Some(10); 2
想把内部数字乘以 2。
可以使用:
1map() 2
1fn main() { 2 let value = Some(10); 3 4 let result = 5 value.map(|number| number * 2); 6 7 println!("{:?}", result); 8} 9
输出:
1Some(20) 2
如果原值是:
1None 2
则结果仍然是:
1None 2
所以 map() 可以理解为:
有值就处理,没有值就继续保持 None。
Option 与字符串处理
例如:
1fn main() { 2 let username = 3 Some(String::from("tom")); 4 5 let result = username.map(|name| { 6 name.to_uppercase() 7 }); 8 9 println!("{:?}", result); 10} 11
输出:
1Some("TOM") 2
使用 and_then()
有时候一次操作之后,仍然可能没有值。
例如:
1fn parse_age(text: &str) 2 -> Option<u32> 3{ 4 text.parse::<u32>().ok() 5} 6
进一步处理:
1fn valid_age(age: u32) 2 -> Option<u32> 3{ 4 if age <= 150 { 5 Some(age) 6 } else { 7 None 8 } 9} 10
可以组合:
1fn main() { 2 let result = parse_age("20") 3 .and_then(valid_age); 4 5 println!("{:?}", result); 6} 7
输出:
1Some(20) 2
如果输入:
1abc 2
或者年龄超过限制,都会得到:
1None 2
使用 Option 查询 Vec
Vec::get() 返回:
1Option<&T> 2
示例:
1fn main() { 2 let numbers = vec![10, 20, 30]; 3 4 match numbers.get(5) { 5 Some(value) => { 6 println!("值:{}", value); 7 } 8 None => { 9 println!("索引不存在"); 10 } 11 } 12} 13
相比:
1numbers[5] 2
get() 不会因为越界直接 panic。
使用 Option 查询 HashMap
1use std::collections::HashMap; 2 3fn main() { 4 let mut users = HashMap::new(); 5 6 users.insert("Tom", 100); 7 8 if let Some(score) = users.get("Tom") { 9 println!("积分:{}", score); 10 } 11} 12
get() 返回:
1Option<&i32> 2
因为对应的键可能不存在。
实战:查找最高成绩
1fn find_max(scores: &[i32]) 2 -> Option<i32> 3{ 4 if scores.is_empty() { 5 return None; 6 } 7 8 let mut max = scores[0]; 9 10 for &score in scores { 11 if score > max { 12 max = score; 13 } 14 } 15 16 Some(max) 17} 18 19fn main() { 20 let scores = [80, 95, 72, 100]; 21 22 match find_max(&scores) { 23 Some(max) => { 24 println!("最高分:{}", max); 25 } 26 None => { 27 println!("没有成绩"); 28 } 29 } 30} 31
如果切片为空:
1[] 2
返回:
1None 2
这样比返回:
10 2
更合理,因为 0 本身可能就是合法成绩。
实战:查询商品价格
1use std::collections::HashMap; 2 3fn find_price( 4 products: &HashMap<String, f64>, 5 name: &str, 6) -> Option<f64> { 7 products.get(name).copied() 8} 9 10fn main() { 11 let mut products = HashMap::new(); 12 13 products.insert( 14 String::from("键盘"), 15 299.0, 16 ); 17 18 products.insert( 19 String::from("鼠标"), 20 99.0, 21 ); 22 23 match find_price(&products, "键盘") { 24 Some(price) => { 25 println!("价格:{:.2}", price); 26 } 27 None => { 28 println!("商品不存在"); 29 } 30 } 31} 32
这里:
1products.get(name) 2
得到:
1Option<&f64> 2
调用:
1copied() 2
可以转换为:
1Option<f64> 2
因为 f64 实现了 Copy。
常见错误
把 Option 当成普通值使用
错误:
1let value = Some(10); 2 3println!("{}", value + 1); 4
必须先处理 Option:
1if let Some(number) = value { 2 println!("{}", number + 1); 3} 4
滥用 unwrap()
不推荐:
1let user = map.get("Tom").unwrap(); 2
如果 Tom 不存在,程序会 panic。
更推荐:
1match map.get("Tom") { 2 Some(user) => { 3 println!("{:?}", user); 4 } 5 None => { 6 println!("用户不存在"); 7 } 8} 9
用特殊值表示“没有数据”
例如:
1fn find_score() -> i32 { 2 -1 3} 4
问题是:
1-1 2
到底表示真实数据,还是不存在?
更加明确的设计是:
1fn find_score() -> Option<i32> 2
最佳实践
如果值可能不存在,优先考虑:
1Option<T> 2
例如:
1fn find_user(id: u64) 2 -> Option<User> 3
不要随意使用:
1unwrap() 2
可以优先考虑:
1match 2if let 3unwrap_or() 4map() 5and_then() 6
函数只需要表达“有没有值”时,Option 非常合适。
但如果不仅要告诉调用者失败,还需要说明:
为什么失败?
那么就应该使用:
1Result<T, E> 2
Option 和 Result 的区别
Option:
1Some(value) 2None 3
表达:
1有值 / 没值 2
例如:
1HashMap::get() 2Vec::get() 3
而 Result:
1Ok(value) 2Err(error) 3
表达:
1成功 / 失败,并携带错误原因 2
例如读取文件:
1成功 -> 文件内容 2失败 -> 文件不存在、权限不足等错误信息 3
可以简单理解:
不需要解释为什么没有值,使用 Option。
需要说明为什么失败,使用 Result。
本章小结
Option<T> 用来安全表达一个值可能存在,也可能不存在。
它包含:
1Some(T) 2
和:
1None 2
通过本文,我们学习了:
- 什么是
Option<T> Some与None- 为什么值不能直接与
Option<T>混用 - 使用
match处理 Option - 使用
if let简化代码 unwrap()与expect()unwrap_or()和unwrap_or_else()- 使用
map()转换值 - 使用
and_then()组合操作 Vec::get()返回 OptionHashMap::get()返回 Option- 使用 Option 设计安全的函数返回值
- Option 和 Result 的区别
可以记住:
Option<T>不是一个需要绕开的麻烦,而是 Rust 强迫我们显式处理“没有数据”这种真实情况。
下一篇预告
下一篇我们将学习 Rust Result 详解:错误处理的核心机制,包括:
- 什么是
Result<T, E> Ok与Err- 使用
match处理错误 unwrap()与expect()unwrap_or()与默认值?运算符- 错误传播
Option与Result的区别parse()错误处理- 文件读取错误处理
- 自定义错误
- 实战:配置解析与业务校验
《Rust Option 详解:安全处理“可能存在,也可能不存在”的值》 是转载文章,点击查看原文。