工程:把代码组织起来
Vec、HashMap 与字符串的三种形态
它在解决什么
前面几章已经零散用过 Vec 和 String,但没系统讲过。
这一篇收掉三件天天会撞到的事:选哪个容器、
entry API 为什么比 get + insert 好、
字符串的四种「字符」各是什么。
Vec<T>:默认选择
let mut v = vec![1, 2, 3];
v.push(4);
v.extend([5, 6]);
Vec::new() 不分配任何内存,容量是 0 ——
第一次 push 才会分配。
⇒ 知道大概要放多少个元素时用 Vec::with_capacity(n),
省掉中途几次重新分配(每次重新分配都要把老数据整个搬一遍)。
⚠️ 那条示例只断言 capacity() >= 1,不断言具体的增长倍数 ——
那是标准库的实现细节,换个版本可能就变了。而「空 Vec 容量是 0」是有保证的。
sort + dedup 是去重的标准组合
dedup 只删相邻的重复。
不先排序的话,[3, 1, 2, 3, 1] 一个元素都删不掉。
⇒ 只要去重不要排序,用 HashSet。
HashMap / BTreeMap / Vec<(K, V)>
三选一的判据只有一句:
| 你要什么 | 用哪个 |
|---|---|
| 最快的查找 | HashMap |
| 按键有序遍历 | BTreeMap |
| 保留插入顺序 | 别用 map —— 用 Vec<(K, V)> |
BTreeMap 按键排序,
所以那条示例输出 a1b2c3 而不是插入顺序 c3a1b2。
🚨 HashMap 的遍历顺序不保证,而且 Rust 每次运行还会换哈希种子
(这是防哈希碰撞攻击的设计)。所以:
- 要稳定输出就自己排;
- 别把
HashMap的遍历顺序写进测试断言 —— 那种测试会随机挂。
entry:一次查找搞定「有就改、没有就插」
*m.entry("a").or_insert(0) += 1;
对比 get + insert 的写法,它只查一次哈希表,而且不用处理 Option。
⚠️ 别忘了前面那个 * ——
or_insert 返回的是 &mut i32,不解引用就是 E0368。
几个常用变体:
| 写法 | 什么时候用 |
|---|---|
or_insert(0) |
默认值很便宜 |
or_insert_with(Vec::new) |
默认值构造起来贵,没用上就别造 |
or_default() |
默认值取类型的 Default |
and_modify(|v| *v += 1).or_insert(1) |
存在和不存在要做不同的事 |
字符串:四种「字符」
这是从别的语言过来最容易出错的地方,因为那些语言通常只有一种。
| 类型 | 是什么 | 大小 |
|---|---|---|
u8 |
一个字节 | 1 |
char |
一个 Unicode 标量值 | 4 字节(不是 1)—— 实测 |
&str |
一段借来的 UTF-8 文本 | 胖指针(地址 + 长度),16 字节 |
String |
拥有的、可增长的 UTF-8 文本 | 地址 + 长度 + 容量,24 字节 |
let s = String::from("héllo");
s.len() // 6 ← 字节
s.chars().count() // 5 ← 字符
换成纯 ASCII 之后两个数就一样了 —— 这正是这类 bug 只在遇到非 ASCII 时才暴露的原因。
⚠️ chars() 数的也不完全等于「用户看到的字符」:
组合字符(e + 重音符)、emoji 序列(👨👩👧)都会比你数的多。
真要按「用户感知的字符」处理,得上 unicode-segmentation 这类库。
String 和 &str 怎么互转
let s: String = "hi".to_string(); // 或 String::from("hi")
let r: &str = &s; // 借用,零成本
let r2: &str = s.as_str(); // 同上,写法更明确
⇒ 参数位置写 &str、存储位置写 String,
理由见切片那篇和
什么时候该 clone。
这一章到这里就完整了
| 篇 | 它回答的问题 |
|---|---|
| 模块、crate 与 workspace | 代码怎么组织,谁能看见谁 |
| 测试与 cargo test | 怎么让它不退化 |
| 本篇 | 那几个天天用的容器怎么选 |
下一步
最后一章讲的是怎么正当地绕开所有权那一章立下的规则 —— 从《智能指针与内部可变性》开始。
全部篇目见 Rust 教程首页。
本篇示例
下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。
`entry` API:一次查找搞定「有就改、没有就插」
use std::collections::HashMap;
fn main() {
let mut m: HashMap<&str, i32> = HashMap::new();
*m.entry("a").or_insert(0) += 1;
*m.entry("a").or_insert(0) += 1;
let mut out: Vec<String> = m.iter().map(|(k, v)| format!("{k}={v}")).collect();
out.sort();
println!("{}", out.join(","));
}编译通过 · 输出 "a=2\n"
对照:忘了前面那个 `*`
use std::collections::HashMap;
fn main() {
let mut m: HashMap<&str, i32> = HashMap::new();
m.entry("a").or_insert(0) += 1;
m.entry("a").or_insert(0) += 1;
let mut out: Vec<String> = m.iter().map(|(k, v)| format!("{k}={v}")).collect();
out.sort();
println!("{}", out.join(","));
}编译不过:error[E0368]、error[E0368]
`or_insert` 返回的是 `&mut i32`,所以要 `*` 解引用才能加(对照项 `E0368`)。⚠️ 注意最后那个 `out.sort()` 不是装饰:**`HashMap` 的遍历顺序不保证**,而且 Rust 每次运行还会换种子。要稳定输出就得自己排。
要顺序就用 `BTreeMap`
use std::collections::BTreeMap;
fn main() {
let mut m = BTreeMap::new();
m.insert("c", 3);
m.insert("a", 1);
m.insert("b", 2);
let out: Vec<String> = m.iter().map(|(k, v)| format!("{k}{v}")).collect();
println!("{}", out.join(""));
}编译通过 · 输出 "a1b2c3\n"
对照:换成 `Vec<(&str, i32)>`(保留插入顺序)
fn main() {
let m = vec![("c", 3), ("a", 1), ("b", 2)];
let out: Vec<String> = m.iter().map(|(k, v)| format!("{k}{v}")).collect();
println!("{}", out.join(""));
}编译通过,输出:"c3a1b2\n"
`BTreeMap` 按**键排序**遍历(所以是 `a1b2c3` 而不是插入顺序 `c3a1b2`)。⇒ 三选一的判据:要最快查找用 `HashMap`,要有序遍历用 `BTreeMap`,要保留插入顺序就别用 map —— 用 `Vec<(K, V)>`。
`len()` 给的是**字节数**,不是字符数
fn main() {
let s = String::from("héllo");
println!("{} {} {}", s.len(), s.chars().count(), s.bytes().count());
}编译通过 · 输出 "6 5 6\n"
对照:把 `é` 换成普通的 `e`
fn main() {
let s = String::from("hello");
println!("{} {} {}", s.len(), s.chars().count(), s.bytes().count());
}编译通过,输出:"5 5 5\n"
`é` 在 UTF-8 下占 2 个字节,所以 `len()` 是 6 而 `chars().count()` 是 5。对照项换成纯 ASCII 之后三个数就一样了 —— **这正是这类 bug 只在遇到非 ASCII 时才暴露的原因**。⚠️ `chars()` 数的是 Unicode 标量值,也不完全等于「用户看到的字符」(组合字符、emoji 序列会更复杂)。
`dedup` 只删**相邻**的重复
fn main() {
let mut v = vec![3, 1, 2, 3, 1];
v.sort();
v.dedup();
println!("{v:?}");
}编译通过 · 输出 "[1, 2, 3]\n"
对照:去掉 `v.sort()`
fn main() {
let mut v = vec![3, 1, 2, 3, 1];
v.dedup();
println!("{v:?}");
}编译通过,输出:"[3, 1, 2, 3, 1]\n"
对照项**一个元素都没删掉**(输出还是原样),因为那五个数里没有两个相邻的相同。⇒ `sort` + `dedup` 是去重的标准组合;只要去重不要排序,用 `HashSet`。
`Vec::new()` 不分配内存,`with_capacity` 才分配
fn main() {
let mut v: Vec<i32> = Vec::new();
let before = v.capacity();
v.push(1);
println!("{} {}", before, v.capacity() >= 1);
}编译通过 · 输出 "0 true\n"
对照:改成 `Vec::with_capacity(10)`
fn main() {
let mut v: Vec<i32> = Vec::with_capacity(10);
let before = v.capacity();
v.push(1);
println!("{} {}", before, v.capacity() >= 1);
}编译通过,输出:"10 true\n"
⚠️ 这条只断言 `capacity() >= 1`,**不断言具体的增长倍数** —— 那是标准库的实现细节,不同版本可能改。空 `Vec` 容量是 0(不分配)这一点倒是有保证的。⇒ 知道大概要放多少个元素时用 `with_capacity`,省掉中途几次重新分配。