Rust 标准库惯用法:每天都用得上的小技巧
每个厨房都有一个放小工具的抽屉:削皮刀、开瓶器、压蒜器。只用一把大菜刀,其实什么活都能干,但有了这些小工具,干起来更快,也更安全。Rust 的标准库里也有这样一个抽屉。
这一页就带你逛逛这个抽屉。每个技巧都很短:一个生活中的比喻、一个能运行的例子,再加一句“适用场景”,让你知道什么时候该拿它出来用。不用一口气全学会。现在先大致浏览一遍,等你在自己的代码里看到某种写法觉得别扭时,再回来翻一翻。
从 &mut 里把值拿出来:mem::take、replace、swap
Section titled “从 &mut 里把值拿出来:mem::take、replace、swap”想象一个套着垃圾袋的垃圾桶。你可以把装满的袋子拿走,但条件是马上套上一个新袋子。垃圾桶永远不会出现没有袋子的时候。
Rust 也有同样的规矩。如果你手里只有 &mut self,就不能直接把某个字段移动出来,因为那会在结构体里留下一个“洞”。编译器会用 E0507 拦住你。std::mem 里的函数解决这个问题的办法是:总是留点东西在原处。
mem::take(&mut x)拿走值,留下该类型的默认值(空Vec、空String、0、None)。mem::replace(&mut x, new)把new放进去,把旧值交给你。Option::take()拿走Option里的东西,留下None。
use std::mem;
struct Inbox { unread: Vec<String>, status: String, draft: Option<String>,}
impl Inbox { // 交出所有未读消息,原处留下一个空 Vec。 fn read_all(&mut self) -> Vec<String> { mem::take(&mut self.unread) }
// 放入新状态,拿回旧状态。 fn set_status(&mut self, new: &str) -> String { mem::replace(&mut self.status, new.to_string()) }
// 取出草稿(如果有的话),原处留下 None。 fn send_draft(&mut self) -> Option<String> { self.draft.take() }}
fn main() { let mut inbox = Inbox { unread: vec!["一起吃午饭?".to_string(), "发票 #42".to_string()], status: "在线".to_string(), draft: Some("6 点见".to_string()), };
let messages = inbox.read_all(); println!("已读 {:?},仍未读:{:?}", messages, inbox.unread);
let old = inbox.set_status("开会中"); println!("状态原来是“{old}”,现在是“{}”", inbox.status);
println!("已发送:{:?}", inbox.send_draft()); println!("再次发送:{:?}", inbox.send_draft());}输出:
已读 ["一起吃午饭?", "发票 #42"],仍未读:[]状态原来是“在线”,现在是“开会中”已发送:Some("6 点见")再次发送:None整段代码里一个 clone() 都没有:消息和旧状态都是被移动出来的,而不是复制。所以这些函数既安全又便宜。
还有两个“亲戚”凑齐这一套。mem::swap(&mut a, &mut b) 交换两个值,就像把桌上的两个盘子对调一下。Option::replace(new) 把新值放进 Option,再把旧值还给你。
use std::mem;
fn main() { // 一个列表在处理时,另一个列表继续接收新订单。 let mut filling = vec!["订单 1", "订单 2"]; let mut processing: Vec<&str> = Vec::new();
mem::swap(&mut filling, &mut processing); println!("处理中 {processing:?},接收中 {filling:?}");
// Option::replace:放入新值,拿回旧值。 let mut now_playing = Some("歌曲 A"); let previous = now_playing.replace("歌曲 B"); println!("之前 {previous:?},现在 {now_playing:?}");}输出:
处理中 ["订单 1", "订单 2"],接收中 []之前 Some("歌曲 A"),现在 Some("歌曲 B")适用场景:你只有 &mut 访问权限,却需要拿到里面那个值的所有权。
Slice:windows、chunks 和它们的伙伴
Section titled “Slice:windows、chunks 和它们的伙伴”可以把 windows(2) 想成一个沿着尺子滑动的放大镜:每次只看两个刻度,然后往前挪一格。chunks(3) 像是把鸡蛋装进三个一盒的蛋盒:最后一盒可能装不满。chunks_exact(3) 只给你装满的盒子,剩下的鸡蛋放在一边,用 .remainder() 取。
这些方法适用于任何 slice,所以数组和 vector 也都能用。
fn main() { let temps = [12, 15, 14, 19, 21];
// windows(2):每一对相邻元素,互相重叠 for pair in temps.windows(2) { println!("{} -> {}:变化 {}", pair[0], pair[1], pair[1] - pair[0]); }
// chunks(3):每批 3 个,最后一批可能更少 let emails = ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"]; for batch in emails.chunks(3) { println!("发送给 {batch:?}"); }
// chunks_exact(2):只给完整的批次;剩下的在 remainder() 里等着 let seats = [1, 2, 3, 4, 5]; let pairs = seats.chunks_exact(2); let leftover = pairs.remainder(); for pair in pairs { println!("一对 {pair:?}"); } println!("剩下:{leftover:?}");}输出:
12 -> 15:变化 315 -> 14:变化 -114 -> 19:变化 519 -> 21:变化 2发送给 ["[email protected]", "[email protected]", "[email protected]"]发送给 ["[email protected]", "[email protected]"]一对 [1, 2]一对 [3, 4]剩下:[5]还有几个 slice 辅助方法,不用写循环就能回答常见的问题。split_first() 给你第一个元素和剩下的部分,就像老师念出名单上的第一个名字,再把剩下的名单交给助手。split_last() 做的是同样的事,只不过从末尾开始。
fn main() { let command = ["git", "commit", "-m", "fix typo"];
// split_first:第一个元素,以及剩下部分组成的 slice if let Some((program, args)) = command.split_first() { println!("程序:{program},参数:{args:?}"); }
// split_last:最后一个元素,以及它前面的所有元素 if let Some((message, _rest)) = command.split_last() { println!("提交信息:{message}"); }
println!("是 git 吗?{}", command.starts_with(&["git"])); println!("有 -m 吗?{}", command.contains(&"-m"));
// iter().rev():倒着遍历,不改变 slice 本身 let history = ["打开", "编辑", "保存"]; let undo_order: Vec<_> = history.iter().rev().collect(); println!("撤销顺序:{undo_order:?}");}输出:
程序:git,参数:["commit", "-m", "fix typo"]提交信息:fix typo是 git 吗?true有 -m 吗?true撤销顺序:["保存", "编辑", "打开"]split_first 和 split_last 返回的是 Option,因为空 slice 根本没有第一个元素。这就是这里要用 if let 的原因。
适用场景:你需要相邻元素(windows)、分批处理(chunks),或者“开头一个加上其余部分”(split_first)。
天天都会用到的迭代器辅助方法
Section titled “天天都会用到的迭代器辅助方法”迭代器那一页讲了 map、filter、sum 和 collect。下面是工具箱里的下一批工具。
peekable:先看一眼再拿
Section titled “peekable:先看一眼再拿”想象你在发牌。有时候你想先看看下一张牌,再决定要不要抽它。.peekable() 给你的迭代器就能做到这一点:.peek() 让你看下一个元素但不拿走它,.next_if(...) 只在下一个元素通过检查时才拿走它。小型解析器需要的正是这个。
fn main() { let input = "12+345-6"; let mut chars = input.chars().peekable(); let mut tokens: Vec<String> = Vec::new();
while let Some(c) = chars.next() { if c.is_ascii_digit() { let mut number = c.to_string(); // 只有下一个字符也是数字时,才把它拿过来。 while let Some(d) = chars.next_if(|next| next.is_ascii_digit()) { number.push(d); } tokens.push(number); } else { tokens.push(c.to_string()); } }
println!("{tokens:?}");}输出:
["12", "+", "345", "-", "6"]如果没有 peekable,你在找更多数字的时候就会把 + 读出来,然后它就没了。
适用场景:下一个元素决定了你怎么处理当前这个元素。
遍历列表:zip、enumerate、step_by、take_while、skip_while、chain
Section titled “遍历列表:zip、enumerate、step_by、take_while、skip_while、chain”| 方法 | 可以想成 | 适用场景 |
|---|---|---|
zip |
拉链,把两边一齿一齿地扣在一起 | 你有两个本来就成对的列表 |
enumerate |
给清单的每一行编号 | 你既要元素,也要它的索引 |
step_by(n) |
上楼梯时一次跨两级(或 n 级) | 你想要每隔 n 个取一个 |
take_while |
一颗颗吃糖,直到碰上一颗不喜欢的 | 你想要列表的开头部分,直到第一个“不行” |
skip_while |
跳过广告,直到节目开始 | 你想要从第一个“不行”开始的其余部分 |
chain |
把两节火车车厢挂在一起 | 你想把两个列表接成一个 |
fn main() { let names = ["Ana", "Ben", "Chen"]; let scores = [88, 92, 75];
// zip 把两个列表配成对;enumerate 加上一个从 0 开始的计数 for (i, (name, score)) in names.iter().zip(scores.iter()).enumerate() { println!("{}. {name}: {score}", i + 1); }
// step_by:一小时里每隔 15 分钟 let quarters: Vec<u32> = (0..60).step_by(15).collect(); println!("每刻钟:{quarters:?}");
// 对数组调用 into_iter(),拿到的是数字本身 let queue = [3, 5, 8, 12, 4]; let small: Vec<i32> = queue.into_iter().take_while(|n| *n < 10).collect(); let rest: Vec<i32> = queue.into_iter().skip_while(|n| *n < 10).collect(); println!("第一个大数之前:{small:?},从它开始:{rest:?}");
// chain:一个列表接在另一个后面 let morning = ["邮件", "站会"]; let afternoon = ["代码评审", "部署"]; let day: Vec<_> = morning.iter().chain(afternoon.iter()).collect(); println!("{day:?}");}输出:
1. Ana: 882. Ben: 923. Chen: 75每刻钟:[0, 15, 30, 45]第一个大数之前:[3, 5, 8],从它开始:[12, 4]["邮件", "站会", "代码评审", "部署"]注意,take_while 一碰到 12 就彻底停下了。后面的 4 虽然很小,但根本不会被检查。这就是它和 filter 的区别:filter 会检查每一个元素。
回答问题:fold、position、find_map、partition、flat_map
Section titled “回答问题:fold、position、find_map、partition、flat_map”| 方法 | 可以想成 | 适用场景 |
|---|---|---|
sum |
计算器上的加号键 | 你只想要总和 |
fold |
从山坡上滚下的雪球,每滚一步就大一圈 | 你想按自己的规则算出一个结果 |
position |
“Ben 坐在几号座位?” | 你需要第一个匹配项的索引 |
find_map |
一把把试钥匙,直到有一把能开门 | 你对每个元素试一件事,留下第一个成功的结果 |
partition |
把要洗的衣服分进两个篮子 | 你想按一个“是 / 否”问题把一个列表分成两个 |
flat_map |
把几个袋子里的东西全倒在一张桌子上 | 每个元素都会变成好几个元素 |
fn main() { let orders = [12, 40, 7, 55, 30];
// sum:直接加起来 let total: i32 = orders.iter().sum();
// fold:按自己的方式加(这里每个订单还要付 2 元手续费) let with_fees = orders.iter().fold(0, |acc, price| acc + price + 2); println!("总额 {total},含手续费 {with_fees}");
// position:第一个匹配项的索引 println!("第一个超过 50 的订单在索引 {:?}", orders.iter().position(|p| *p > 50));
// partition:两个 Vec,一个装“是”,一个装“否” let (big, small): (Vec<i32>, Vec<i32>) = orders.into_iter().partition(|p| *p >= 30); println!("大单 {big:?},小单 {small:?}");
// find_map:挨个尝试解析,留下第一个成功的 let inputs = ["abc", "", "42", "7"]; let first_number = inputs.iter().find_map(|s| s.parse::<i32>().ok()); println!("第一个数字:{first_number:?}");
// flat_map:每一行变成好几个单词,全部放进一个列表 let lines = ["buy milk", "call mum now"]; let words: Vec<&str> = lines.iter().flat_map(|line| line.split(' ')).collect(); println!("{words:?}");}输出:
总额 144,含手续费 154第一个超过 50 的订单在索引 Some(3)大单 [40, 55, 30],小单 [12, 7]第一个数字:Some(42)["buy", "milk", "call", "mum", "now"](flat_map 例子里的句子保留了英文,因为它按空格拆分单词,而中文句子的词与词之间没有空格。)
fold 接收一个初始值(0)和一个闭包,闭包每次拿到“目前为止的结果”和下一个元素。如果你只需要一个普通的总和,sum 表达得更清楚。规则是你自己定的时候,再拿出 fold。
就地清理 Vec:dedup 和 retain
Section titled “就地清理 Vec:dedup 和 retain”这两个是 Vec 的方法,不是迭代器方法,但它们也属于这个抽屉。dedup 像一个从不连着放同一首歌的 DJ(这首歌之后还可以再放)。retain 像门口的保安:只有通过检查的元素才能留在里面。
fn main() { // dedup 删除紧挨在一起的重复项 let mut clicks = vec!["首页", "首页", "商店", "商店", "商店", "购物车", "首页"]; clicks.dedup(); println!("{clicks:?}");
// retain 只保留通过检查的元素 let mut cart = vec![("苹果", 3), ("面包", 0), ("牛奶", 1), ("鸡蛋", 0)]; cart.retain(|(_, qty)| *qty > 0); println!("{cart:?}");}输出:
["首页", "商店", "购物车", "首页"][("苹果", 3), ("牛奶", 1)]最后那个 "首页" 留了下来,因为它旁边没有另一个 "首页"。要删掉所有重复项,得先排序,排序那一页有示范。
适用场景:你想修改一个 Vec,而不想另外构建一个新的。
Option 和 Result 的便捷方法
Section titled “Option 和 Result 的便捷方法”Option 就像一个可能到了、也可能还没到的包裹。你可以每个包裹都用完整的 match 来拆,但大多数时候,一个单词的辅助方法就能说清同样的意思。Option 那一页讲了 unwrap_or 和 match;下面是更多辅助方法。
fn main() { let nickname: Option<String> = None; let age: Option<u32> = Some(17); let width: Option<u32> = Some(3); let height: Option<u32> = Some(4);
// unwrap_or_default:取出值,或者该类型的默认值(""、0、空 Vec……) println!("昵称:“{}”", nickname.unwrap_or_default());
// map_or:None 时用备用值,否则转换里面的值 println!("{}", age.map_or("年龄未知".to_string(), |a| format!("{a} 岁")));
// filter:只有通过检查才保留这个值 println!("成年人的年龄:{:?}", age.filter(|a| *a >= 18));
// zip:只有两个都是 Some 才得到 Some println!("面积:{:?}", width.zip(height).map(|(w, h)| w * h));
// and_then:串起一个本身也可能得到 None 的步骤 let good = Some("42").and_then(|s| s.parse::<u32>().ok()); let bad = Some("四十").and_then(|s| s.parse::<u32>().ok()); println!("解析结果:{good:?} 和 {bad:?}");}输出:
昵称:“”17 岁成年人的年龄:None面积:Some(12)解析结果:Some(42) 和 None在函数里,? 也能用在 Option 上:如果值是 None,函数会立刻返回 None。在返回 Result 的函数里,要先用 ok_or(error) 或 ok_or_else(|| error) 把 Option 转成 Result。如果构造错误需要花点功夫,就用 ok_or_else,因为它只在真正需要时才运行。
struct Employee { name: String, manager: Option<String>,}
// 对 Option 用 `?`:如果是 None,立刻返回 None。fn manager_initial(e: &Employee) -> Option<char> { let manager = e.manager.as_ref()?; manager.chars().next()}
// ok_or_else 把 None 变成一个错误,这样 `?` 就能在返回 Result 的函数里用了。fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting.ok_or_else(|| "没有设置 PORT".to_string())?; raw.parse::<u16>().map_err(|e| e.to_string())}
fn main() { let ana = Employee { name: "Ana".to_string(), manager: Some("Grace".to_string()) }; let grace = Employee { name: "Grace".to_string(), manager: None }; println!("{}: {:?}", ana.name, manager_initial(&ana)); println!("{}: {:?}", grace.name, manager_initial(&grace));
println!("{:?}", port_from(Some("8080"))); println!("{:?}", port_from(None)); println!("{:?}", port_from(Some("八十")));
// transpose:Option<Result<..>> 变成 Result<Option<..>> let retries_setting: Option<&str> = Some("3"); let retries: Result<Option<u8>, _> = retries_setting.map(|s| s.parse::<u8>()).transpose(); println!("{retries:?}");}输出:
Ana: Some('G')Grace: NoneOk(8080)Err("没有设置 PORT")Err("invalid digit found in string")Ok(Some(3))invalid digit found in string(字符串里有无效的数字)是标准库自己给出的错误信息,所以是英文的。transpose 用得不多,但它正好漂亮地解决了“我有一个可选的设置项,而解析它可能失败”这种情况。有了它,你可以在结果上用 ?,同时保留 Option。
适用场景:对 Option 做 match 时,只有一个分支有意思,另一个分支很无聊。
HashMap 的 entry API
Section titled “HashMap 的 entry API”想想衣帽寄存处。你报一个名字,服务员要么找到那个挂钩,要么新挂一个,然后交给你。跑一趟就够了,不用跑两趟。这就是 .entry(key)。哈希 map 那一页介绍了 entry(key).or_insert(value),也就是经典的计数器写法。还有两个变体值得了解:
or_insert_with(f)只在键不存在时才构造默认值。or_default()做同样的事,用的是该类型的默认值。and_modify(f)在键存在时修改它的值;在后面接上or_insert,处理“新键”的情况。
use std::collections::HashMap;
fn main() { let sales = [("苹果", 3), ("梨", 2), ("苹果", 5), ("李子", 1), ("梨", 4)];
// and_modify + or_insert:存在就更新,不存在就插入 let mut totals: HashMap<&str, i32> = HashMap::new(); for (fruit, qty) in sales { totals.entry(fruit).and_modify(|t| *t += qty).or_insert(qty); }
// or_insert_with:只在需要时才创建空 Vec let mut orders: HashMap<&str, Vec<i32>> = HashMap::new(); for (fruit, qty) in sales { orders.entry(fruit).or_insert_with(Vec::new).push(qty); }
// HashMap 的顺序是随机的,所以打印前先给键排序 let mut fruits: Vec<&str> = totals.keys().copied().collect(); fruits.sort(); for fruit in fruits { println!("{fruit}:总计 {},订单 {:?}", totals[fruit], orders[fruit]); }}输出:
李子:总计 1,订单 [1]梨:总计 6,订单 [2, 4]苹果:总计 8,订单 [3, 5](字符串排序按的是 Unicode 码位,不是拼音,所以“李子”排在了“苹果”前面。)
适用场景:不然你就得写“如果键存在就修改,否则就插入”,那样会把键查找两遍。
never 类型 !:永远不会回来的代码
Section titled “never 类型 !:永远不会回来的代码”有些路是单行道:开进去以后,就再也出不来了。在 Rust 里,永远不会结束的表达式具有 never 类型,写作 !。panic!、return、break、continue、无限 loop 以及 std::process::exit 都是这种类型。
因为 ! 类型的值永远不可能真正存在,编译器允许它冒充任何类型。所以一个 match 可以有一个分支给出 i32,另一个分支执行 return:第二个分支永远不会产生值,也就不会破坏类型。
use std::process;
// `-> !` 表示“这个函数永远不会返回”fn quit(message: &str) -> ! { println!("{message}"); process::exit(0)}
fn level_points(level: &str) -> u32 { let points: u32 = match level { "easy" => 10, "hard" => 30, _ => return 0, // `return` 的类型是 `!`,所以能放在需要 u32 的地方 }; points * 2}
fn main() { println!("easy:{},hard:{},weird:{}", level_points("easy"), level_points("hard"), level_points("weird"));
let mut total = 0; for input in ["4", "oops", "6"] { let n: i32 = match input.parse() { Ok(n) => n, Err(_) => continue, // `continue` 的类型也是 `!` }; total += n; } println!("合计:{total}");
let config_file: Option<&str> = None; let config = match config_file { Some(path) => path, None => quit("找不到配置文件,停止运行"), }; println!("已加载 {config}"); // 永远不会打印}输出:
easy:20,hard:60,weird:0合计:10找不到配置文件,停止运行提醒一句:process::exit 会立刻结束程序,不会为还活着的值运行清理代码(析构函数)。在真实的程序里,从 main 正常返回通常是更温和的选择。
有两个宏在你写代码草稿时把 ! 当作占位符来用。todo!() 的意思是“这里以后再写”,unimplemented!() 的意思是“这里不支持”。一旦真的执行到它们,两者都会 panic,但它们能让程序的其余部分照常编译:
fn export_pdf(report: &str) -> Vec<u8> { todo!("PDF 导出功能将在 v2 推出")}调用它的话,你会看到 not yet implemented: PDF 导出功能将在 v2 推出。换成 unimplemented!("PDF 导出"),信息就是 not implemented: PDF 导出。前半部分的英文是 Rust 自己加上的。
适用场景:某个函数应该让程序停下来,或者某个分支应该提前离开,而不是给出一个值。
顺手的小工具
Section titled “顺手的小工具”这些是放在口袋里的工具:很小,但你每周都会用到。
matches!(value, pattern)用true或false回答“这个值符合这个模式吗?”(详见模式匹配那一页)。std::cmp::min/max选出两个值中较小或较大的那个,.clamp(low, high)把值限制在一个范围内,就像一个转到 100 就转不动的音量旋钮。.abs_diff(other)给出两个数之间的距离。结果总是正数,而且永远不会溢出,就算是无符号类型也一样。dbg!(expr)打印文件名、行号、表达式和值,然后把这个值原样还给你,所以你可以把它套在任何表达式外面。assert!(condition)在条件为 false 时 panic。debug_assert!做的是同样的事,但只在 debug 构建里检查,所以在 release 构建里没有任何开销。
use std::cmp::{max, min};
enum Order { Pending, Shipped, Delivered, Cancelled,}
fn main() { let orders = [Order::Pending, Order::Delivered, Order::Shipped, Order::Cancelled, Order::Pending];
// matches!:是 / 否的模式检查,用 | 表示“或” let open = orders.iter().filter(|o| matches!(o, Order::Pending | Order::Shipped)).count(); println!("未完成的订单:{open}");
// min、max 和 clamp println!("最小 {},最大 {}", min(7, 3), max(7, 3)); let volume = 140; let safe = volume.clamp(0, 100); println!("音量 {volume} 被限制为 {safe}");
// abs_diff:相差多少,永远不会是负数 let (guess, secret): (u32, u32) = (30, 42); println!("你还差 {}", guess.abs_diff(secret));
// dbg! 打印到 stderr,并把值还回来 let doubled = dbg!(safe * 2);
// 只要这个条件为 false,assert! 就会让程序停下 assert!(doubled <= 200, "翻倍后的音量太大:{doubled}"); debug_assert!(safe <= 100); // 只在 debug 构建里检查 println!("所有检查都通过了");}输出:
未完成的订单:3最小 3,最大 7音量 140 被限制为 100你还差 12所有检查都通过了上面的输出里没有 dbg! 那一行,因为 dbg! 写到的是标准错误(stderr),而不是标准输出(stdout)。在 Playground 里,它会出现在“Standard Error”面板中:
[src/main.rs:28:19] safe * 2 = 200这也是为什么调试时到处撒 dbg! 很安全:它不会混进程序真正的输出里。只是记得在发布前把它删掉。
排序也有自己的一套小工具。排序那一页讲得很详细,这里只说个简短版。sort_unstable 比 sort 稍快一点,给普通数字排序完全够用。sort_by_key 按某一个字段排序。binary_search 能在已排序的数据里飞快地找到一个元素,就像查字典时从中间翻开,而不是从第一页读起。
struct Player { name: &'static str, score: u32,}
fn main() { // sort_unstable:稍快一些;相等的元素可能互换位置 let mut ids = vec![42, 7, 19, 3, 25]; ids.sort_unstable(); println!("{ids:?}");
// binary_search:找到就返回 Ok(索引),找不到就返回 Err(它应该插入的位置) println!("查找 19:{:?}", ids.binary_search(&19)); println!("查找 20:{:?}", ids.binary_search(&20));
// sort_by_key:按某一个字段排序 let mut players = vec![ Player { name: "Ana", score: 120 }, Player { name: "Ben", score: 95 }, Player { name: "Chen", score: 150 }, ]; players.sort_by_key(|p| p.score); let names: Vec<&str> = players.iter().map(|p| p.name).collect(); println!("从低到高:{names:?}");}输出:
[3, 7, 19, 25, 42]查找 19:Ok(2)查找 20:Err(3)从低到高:["Ben", "Ana", "Chen"]Err(3) 并不是什么需要担心的失败。它告诉你 20 不在列表里,而索引 3 正是插入它之后列表依然有序的位置。
适用场景:你发现自己在写一个小小的 if 或循环,而这些工具里已经有现成的。
对没有实现 Default 的类型使用 mem::take。mem::take 必须在原处留下点什么,所以它得知道这个类型的“空”是什么样子:
use std::mem;
struct Connection { host: String,}
struct Pool { active: Connection,}
impl Pool { fn reset(&mut self) -> Connection { mem::take(&mut self.active) // ❌ 一个“空的” Connection 应该是什么样? }}error[E0277]: the trait bound `Connection: Default` is not satisfied --> src/main.rs:13:19 |13 | mem::take(&mut self.active) // ❌ 一个“空的” Connection 应该是什么样? | --------- ^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Connection` | | | required by a bound introduced by this call |help: consider annotating `Connection` with `#[derive(Default)]`编译器说的是“Connection 没有实现 Default trait”,并建议你加上 #[derive(Default)]。一种修法是自己选好替换的值:mem::replace(&mut self.active, Connection { host: "db2".to_string() });另一种是把字段存成 Option<Connection>,然后用 self.active.take()。只有当“空”值对这个类型真的有意义时,才去加 #[derive(Default)]。
在返回 Result 的函数里对 Option 用 ?。这两者不会自动混用:
fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting?; // ❌ None 没有可以返回的错误信息 raw.parse::<u16>().map_err(|e| e.to_string())}error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result` --> src/main.rs:2:22 |1 | fn port_from(setting: Option<&str>) -> Result<u16, String> { | ---------------------------------------------------------- this function returns a `Result`2 | let raw = setting?; // ❌ None 没有可以返回的错误信息 | ^ use `.ok_or(...)?` to provide an error compatible with `Result<u16, String>`意思是:“在返回 Result 的函数里,? 运算符只能用在 Result 上,不能用在 Option 上。”编译器其实已经告诉你怎么修了:写成 setting.ok_or("没有设置 PORT".to_string())?,或者像上面的例子那样用 ok_or_else。这个错误的其他形式见 E0277。
以为 dedup 或 binary_search 能处理未排序的数据。dedup 只删除紧挨在一起的重复项,而对未排序的数据做 binary_search,得到的答案毫无意义。这两种情况都不会产生编译错误,所以很容易漏掉。记得先排序。
忘了 chunks_exact 剩下的元素。对 chunks_exact(3) 做 for 循环,会悄悄跳过最后 1 到 2 个元素。如果它们很重要,就处理一下 .remainder(),或者改用 chunks。
1. 每日价格变化
Section titled “1. 每日价格变化”你有一组价格 [100, 104, 98, 110, 107]。构建一个 Vec,存放每一天到下一天的价格变化,把它打印出来,再打印最大的涨幅。提示:windows(2) 会把每一对价格作为一个小 slice w 给你,所以变化就是 w[1] - w[0]。.iter().max() 能找到最大值。
查看答案
fn main() { let prices = [100, 104, 98, 110, 107];
let changes: Vec<i32> = prices.windows(2).map(|w| w[1] - w[0]).collect(); println!("每日变化:{changes:?}");
let biggest_rise = changes.iter().max(); println!("最大涨幅:{biggest_rise:?}");}输出:
每日变化:[4, -6, 12, -3]最大涨幅:Some(12)五个价格得到四个变化,因为 windows(2) 每走一步只产生一对。max() 返回的是 Option,因为空列表里没有最大值。
2. 清空打印队列
Section titled “2. 清空打印队列”写一个 Printer 结构体,带一个 queue: Vec<String> 字段。给它一个 add(&mut self, file: &str) 方法,再写一个 flush(&mut self) -> Vec<String> 方法,把队列里的所有文件交出来,并让队列变空。加入两个文件,执行 flush,然后打印清出来的文件,以及队列里还剩几个文件。提示:flush 只有 &mut self,所以要用 std::mem::take。
查看答案
use std::mem;
struct Printer { queue: Vec<String>,}
impl Printer { fn add(&mut self, file: &str) { self.queue.push(file.to_string()); }
fn flush(&mut self) -> Vec<String> { mem::take(&mut self.queue) }}
fn main() { let mut printer = Printer { queue: Vec::new() }; printer.add("report.pdf"); printer.add("photo.png");
let jobs = printer.flush(); println!("正在打印 {} 个任务:{jobs:?}", jobs.len()); println!("队列中还剩:{}", printer.queue.len());}输出:
正在打印 2 个任务:["report.pdf", "photo.png"]队列中还剩:0如果在 flush 里只写 self.queue,就会得到 E0507 错误。mem::take 把 Vec 移出来,原处留下一个空的,完全不需要复制。
3. 第一个有效端口
Section titled “3. 第一个有效端口”写一个函数 first_port(candidates: &[&str]) -> Option<u16>,返回第一个能解析成 u16 的字符串。分别用 ["", "abc", "70000", "8080", "9090"] 和 ["none", "-1"] 调用它,打印找到的端口;如果一个都没有,就打印 3000。提示:用 find_map 配合 s.parse::<u16>().ok(),再接 unwrap_or(3000)。
查看答案
fn first_port(candidates: &[&str]) -> Option<u16> { candidates.iter().find_map(|s| s.parse::<u16>().ok())}
fn main() { let from_config = ["", "abc", "70000", "8080", "9090"]; let all_bad = ["none", "-1"];
println!("使用端口 {}", first_port(&from_config).unwrap_or(3000)); println!("使用端口 {}", first_port(&all_bad).unwrap_or(3000));}输出:
使用端口 8080使用端口 3000"70000" 看起来是个数字,但它放不进 u16(最大是 65535),所以解析失败,find_map 接着往下找。它在 "8080" 停下,根本不会去看 "9090"。
mem::take、mem::replace和Option::take通过在原处留下别的东西,把值从&mut里移出来;mem::swap交换两个值。- 在 slice 上,
windows给出互相重叠的相邻元素,chunks给出一批批的元素,split_first/split_last给出“一个元素加上其余部分”。 peekable让你提前看一眼;zip、enumerate、chain、fold、find_map和partition能代替许多手写的循环。unwrap_or_default、map_or、filter和ok_or这些Option辅助方法,省得你写一个个小match;?也能用在Option上。entry()配合or_insert_with或and_modify,只查找一次就能更新HashMap。- never 类型
!属于永远不会结束的代码(panic!、return、continue、process::exit),所以它能放在任何需要值的地方。
小测验
1. 当 items 是 Vec 时,mem::take(&mut self.items) 会在原处留下什么?
2. [1, 2, 3, 4].windows(2) 会给你什么?
3. 为什么 let x: i32 = match s { "a" => 1, _ => return }; 能通过编译?
4. 你的函数返回 Result,但手里拿的是 Option。在 ? 前面应该加什么?
得分: 0 / 4
std::mem— 标准库文档(英文):take、replace、swap以及其他内存相关的辅助函数,带示例。- Slice 的方法 — 标准库文档(英文):slice 的所有方法,包括
windows、chunks、split_first和binary_search。 Iteratortrait — 标准库文档(英文):迭代器方法的完整列表,从zip、peekable到partition、find_map。Option— 标准库文档(英文):Option的所有辅助方法,按用途分组。- 发散函数 — 通过例子学 Rust:返回
!的函数的简短示例,以及它为什么有用。