常用集合
你会学到什么
Vec<T>是可增长的数组:push/pop、下标与get、迭代、retain过滤、排序。String是可增长的 UTF-8 文本:用push_str与format!构建。HashMap<K, V>用entryAPI 优雅地“查找或插入”,常用来计数。HashSet<T>用来去重与做集合运算(交集等)。
最小示例
use std::collections::HashMap;
let mut counts: HashMap<String, usize> = HashMap::new();
*counts.entry("a".to_string()).or_insert(0) += 1;
运行代码
cd examples
cargo run -p rt_13_collections
cargo test -p rt_13_collections
代码组织
本示例按集合类型拆成多个文件:库 crate(lib.rs)声明并重导出各模块,main.rs 只负责调用与演示输出。
src/
├── lib.rs # mod 声明 + pub use 重导出
├── vectors.rs # Vec:summarize / passing_sorted,以及 Score 结构体
├── strings.rs # String:format_scores
├── maps.rs # HashMap:word_count / char_frequency;HashSet:sorted_unique / common_count
└── main.rs # 精简入口:use rt_13_collections::...; 运行演示
每个模块的单元测试就放在各自文件的 #[cfg(test)] 内,便于就近维护。
代码讲解
示例围绕“成绩”这个小场景,把四种集合串起来。下方完整源码已自动嵌入,这里只点出关键之处。
Vec:摘要、过滤与排序
summarize 用迭代器对 Vec 做汇总:iter().sum() 求和、iter().max() 取最大值,配合 len() 算平均。注意空切片时要手动避免除零。
passing_sorted 展示两个高频方法:retain 原地过滤(只保留满足条件的元素),sort_by 用闭包自定义排序——这里用 b.cmp(a) 实现降序。
下标 v[i] 越界会 panic,而 v.get(i) 返回 Option<&T>,越界时是 None,更安全。
String:用 push_str 与 format! 构建文本
format_scores 把多条记录拼成一行:push_str 追加字符串切片,format! 生成带格式的临时字符串。enumerate 用来在非首项前插入 ", " 分隔符。
HashMap:entry 计数
entry(key).or_insert(default) 返回值的可变引用:键不存在时插入默认值,存在时直接拿到。这让计数器变得很简洁:
for word in text.split_whitespace() {
*counts.entry(word.to_string()).or_insert(0) += 1;
}
char_frequency 同样用这个套路,把键换成 char,统计字符出现次数。注意 HashMap 的迭代顺序不固定,示例在打印前先收集成 Vec 再排序,输出才稳定。
HashSet:去重与交集
Vec 的排序去重组合很常见,但 dedup 只能去掉相邻重复,所以要先排序:
let mut result = values.to_vec();
result.sort_unstable();
result.dedup();
如果不关心顺序,HashSet 更直接。common_count 把两个切片各自收进 HashSet,再用 intersection 求公共元素个数。
常见错误
get 返回的是 Option<&V> 而不是 V:
let n = counts.get("a") + 1; // ❌ 不能直接对 Option 运算
用 counts.get("a").copied().unwrap_or(0) 取出值或默认值。
练习
- 用
HashMap统计一段文本里每个字符的出现次数。 - 写一个函数合并两个
Vec<i32>并返回排序去重后的结果。
小结
Vec、HashMap、String 是日常 Rust 的三大主力集合,HashSet 适合去重;entry API 是 HashMap 的高频技巧,retain/sort_by 则是 Vec 的常用利器。
下一步
真实程序会遇到失败。下一章学习用 Result 和 ? 处理错误。
完整示例代码
下面是 examples/13_collections/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。
examples/13_collections/src/main.rs
//! 演示 `Vec`、`String`、`HashMap` 与 `HashSet` 的常见用法。
//!
//! 具体函数实现见库 crate `rt_13_collections` 的各模块。
use rt_13_collections::{
Score, char_frequency, common_count, format_scores, passing_sorted, sorted_unique, summarize,
word_count,
};
fn main() {
// ---------- Vec:摘要与过滤 ----------
let raw = [90, 55, 72, 60, 48, 88];
println!("== Vec ==");
println!("原始分数: {raw:?}");
let (count, total, max, avg) = summarize(&raw);
println!("数量={count}, 总分={total}, 最高={max}, 平均={avg:.1}");
println!("及格并降序: {:?}", passing_sorted(&raw));
// push / pop / 下标访问 / 安全访问
let mut stack = vec![10, 20, 30];
stack.push(40);
println!("push 后: {stack:?}");
println!("stack[0] = {}", stack[0]); // 下标越界会 panic
println!("stack.get(99) = {:?}", stack.get(99)); // 越界返回 None
println!("pop 出: {:?}", stack.pop());
// ---------- String:构建文本 ----------
println!("\n== String ==");
let scores = vec![
Score {
name: "Alice".to_string(),
points: 90,
},
Score {
name: "Bob".to_string(),
points: 55,
},
];
println!("成绩单: {}", format_scores(&scores));
// ---------- HashMap:计数 ----------
println!("\n== HashMap ==");
let counts = word_count("the cat the dog the bird");
println!("'the' 出现 {} 次", counts.get("the").copied().unwrap_or(0));
// 排序后输出,保证演示结果稳定(HashMap 迭代顺序不固定)
let mut pairs: Vec<_> = counts.iter().collect();
pairs.sort();
println!("全部词频: {pairs:?}");
let freq = char_frequency("hello");
println!("'l' 出现 {} 次", freq.get(&'l').copied().unwrap_or(0));
// ---------- HashSet:去重与交集 ----------
println!("\n== HashSet ==");
let unique = sorted_unique(&[3, 1, 2, 3, 1]);
println!("排序去重: {unique:?}");
println!(
"[1,2,3,4] 与 [3,4,5] 的公共元素个数: {}",
common_count(&[1, 2, 3, 4], &[3, 4, 5])
);
} examples/13_collections/src/lib.rs
//! 常用集合:`Vec<T>`、`String` 与 `HashMap<K, V>`(外加一点 `HashSet`)。
//!
//! 这三种集合是日常 Rust 的主力,本 crate 按集合类型拆成三个模块:
//! - [`vectors`]:`Vec<T>` 的摘要、过滤排序,以及成绩记录类型 [`Score`]。
//! - [`strings`]:`String` 文本拼接(`push_str` / `format!`)。
//! - [`maps`]:`HashMap<K, V>` 计数与 `HashSet<T>` 去重、集合运算。
//!
//! 运行 `cargo run -p rt_13_collections` 查看带标签的输出。
pub mod maps;
pub mod strings;
pub mod vectors;
pub use maps::{char_frequency, common_count, sorted_unique, word_count};
pub use strings::format_scores;
pub use vectors::{Score, passing_sorted, summarize}; examples/13_collections/src/maps.rs
//! `HashMap<K, V>` 与 `HashSet<T>` 相关:计数、去重与集合运算。
//!
//! - `HashMap<K, V>`:键值映射,`entry` API 让“查找或插入”非常优雅。
//! - `HashSet<T>`:快速成员判断与集合运算(交集、并集等)。
use std::collections::{HashMap, HashSet};
/// 统计每个单词出现的次数。
///
/// `entry(key).or_insert(0)` 返回值的可变引用:键不存在时插入 `0`,
/// 存在时直接拿到,于是 `+= 1` 就完成了计数。
pub fn word_count(text: &str) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word.to_string()).or_insert(0) += 1;
}
counts
}
/// 统计每个字符出现的次数(忽略空白字符)。
///
/// 演示 `String`/`&str` 的 `chars` 迭代与 `HashMap<char, usize>`。
pub fn char_frequency(text: &str) -> HashMap<char, usize> {
let mut freq = HashMap::new();
for ch in text.chars().filter(|c| !c.is_whitespace()) {
*freq.entry(ch).or_insert(0) += 1;
}
freq
}
/// 排序去重,返回新的 `Vec`。
///
/// `dedup` 只能去掉**相邻**重复,所以必须先 `sort` 才能彻底去重;
/// 这里也演示了如何用 `HashSet` 达到同样效果(但顺序不保证)。
pub fn sorted_unique(values: &[i32]) -> Vec<i32> {
let mut result = values.to_vec();
result.sort_unstable();
result.dedup();
result
}
/// 用 `HashSet` 求两个切片的去重交集元素个数。
///
/// 演示 `HashSet` 的快速成员判断与集合运算。
pub fn common_count(a: &[i32], b: &[i32]) -> usize {
let set_a: HashSet<i32> = a.iter().copied().collect();
let set_b: HashSet<i32> = b.iter().copied().collect();
set_a.intersection(&set_b).count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_words() {
let counts = word_count("a b a c a");
assert_eq!(counts.get("a"), Some(&3));
assert_eq!(counts.get("b"), Some(&1));
assert_eq!(counts.get("z"), None);
}
#[test]
fn counts_chars() {
let freq = char_frequency("hello world");
assert_eq!(freq.get(&'l'), Some(&3));
assert_eq!(freq.get(&'o'), Some(&2));
assert_eq!(freq.get(&' '), None); // 空白被忽略
}
#[test]
fn sorts_and_dedups() {
assert_eq!(sorted_unique(&[3, 1, 2, 3, 1]), vec![1, 2, 3]);
assert_eq!(sorted_unique(&[]), Vec::<i32>::new());
}
#[test]
fn counts_common_elements() {
assert_eq!(common_count(&[1, 2, 3, 4], &[3, 4, 5]), 2);
assert_eq!(common_count(&[1, 2], &[3, 4]), 0);
}
} examples/13_collections/src/strings.rs
//! `String` 相关:把结构化数据拼接成可读文本。
//!
//! `String` 是可增长的 UTF-8 文本,可用 `push_str` / `format!` 拼接。
use crate::vectors::Score;
/// 把一组成绩拼成一行可读文本,例如 `"Alice: 90, Bob: 55"`。
///
/// 演示 `String` 的构建:`push_str` 与 `format!`。
pub fn format_scores(scores: &[Score]) -> String {
let mut line = String::new();
for (i, score) in scores.iter().enumerate() {
if i > 0 {
line.push_str(", ");
}
line.push_str(&format!("{}: {}", score.name, score.points));
}
line
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formats_scores_line() {
let scores = vec![
Score {
name: "Alice".to_string(),
points: 90,
},
Score {
name: "Bob".to_string(),
points: 55,
},
];
assert_eq!(format_scores(&scores), "Alice: 90, Bob: 55");
assert_eq!(format_scores(&[]), "");
}
} examples/13_collections/src/vectors.rs
//! `Vec<T>` 相关:摘要、过滤排序,以及成绩记录类型 `Score`。
//!
//! `Vec<T>` 是可增长的数组,支持下标、迭代、过滤与排序。
/// 一次成绩记录:学生名字与分数。
#[derive(Debug, Clone)]
pub struct Score {
pub name: String,
pub points: u32,
}
/// 对一组分数做摘要:数量、总和、最大值与平均值。
///
/// 演示 `Vec` 的迭代器:`iter` / `sum` / `max` / 长度。
/// 空切片时平均值返回 `0.0`,避免除零。
pub fn summarize(scores: &[u32]) -> (usize, u32, u32, f64) {
let count = scores.len();
let total: u32 = scores.iter().sum();
let max = scores.iter().copied().max().unwrap_or(0);
let avg = if count == 0 {
0.0
} else {
f64::from(total) / count as f64
};
(count, total, max, avg)
}
/// 保留及格(>= 60)的分数,并从高到低排序,返回新的 `Vec`。
///
/// 演示 `retain`(原地过滤)与 `sort_by`(自定义排序)。
pub fn passing_sorted(scores: &[u32]) -> Vec<u32> {
let mut result = scores.to_vec();
result.retain(|&s| s >= 60);
result.sort_by(|a, b| b.cmp(a)); // 降序
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn summarizes_scores() {
let (count, total, max, avg) = summarize(&[10, 20, 30]);
assert_eq!(count, 3);
assert_eq!(total, 60);
assert_eq!(max, 30);
assert!((avg - 20.0).abs() < f64::EPSILON);
}
#[test]
fn summarize_handles_empty() {
let (count, total, max, avg) = summarize(&[]);
assert_eq!((count, total, max), (0, 0, 0));
assert_eq!(avg, 0.0);
}
#[test]
fn keeps_passing_and_sorts_desc() {
assert_eq!(passing_sorted(&[90, 55, 60, 48]), vec![90, 60]);
assert_eq!(passing_sorted(&[10, 20]), Vec::<u32>::new());
}
} examples/13_collections/Cargo.toml
[package]
name = "rt_13_collections"
version.workspace = true
edition.workspace = true
publish.workspace = true