标准库与错误处理 beginner 30 分钟 更新 2026-06-15

常用集合

掌握 Vec、HashMap 和 String 的常见用法。

常用集合

你会学到什么

  • Vec<T> 是可增长的数组:push/pop、下标与 get、迭代、retain 过滤、排序。
  • String 是可增长的 UTF-8 文本:用 push_strformat! 构建。
  • HashMap<K, V>entry API 优雅地“查找或插入”,常用来计数。
  • 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_strformat! 构建文本

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> 并返回排序去重后的结果。

小结

VecHashMapString 是日常 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