跳转到内容

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 个,最后一批可能更少
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:变化 3
15 -> 14:变化 -1
14 -> 19:变化 5
19 -> 21:变化 2
一对 [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() 给你的迭代器就能做到这一点:.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: 88
2. Ben: 92
3. 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 像一个从不连着放同一首歌的 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 就像一个可能到了、也可能还没到的包裹。你可以每个包裹都用完整的 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: None
Ok(8080)
Err("没有设置 PORT")
Err("invalid digit found in string")
Ok(Some(3))
▶ 动手试一试

invalid digit found in string(字符串里有无效的数字)是标准库自己给出的错误信息,所以是英文的。transpose 用得不多,但它正好漂亮地解决了“我有一个可选的设置项,而解析它可能失败”这种情况。有了它,你可以在结果上用 ?,同时保留 Option。

适用场景:对 Option 做 match 时,只有一个分支有意思,另一个分支很无聊。

想想衣帽寄存处。你报一个名字,服务员要么找到那个挂钩,要么新挂一个,然后交给你。跑一趟就够了,不用跑两趟。这就是 .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 自己加上的。

适用场景:某个函数应该让程序停下来,或者某个分支应该提前离开,而不是给出一个值。

这些是放在口袋里的工具:很小,但你每周都会用到。

  • 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。

你有一组价格 [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,因为空列表里没有最大值。

写一个 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 移出来,原处留下一个空的,完全不需要复制。

写一个函数 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