Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

rust 容器

Rust 标准库提供了很多常用容器。本章先介绍最常见的四个:

  • String:拥有所有权的 UTF-8 字符串。
  • Vec<T>:连续内存上的动态数组。
  • HashMap<K, V>:基于哈希表的键值映射。
  • VecDeque<T>:双端队列,适合头尾两端高效插入和删除。

这几个容器都遵守 Rust 的所有权规则。理解它们不仅是学习标准库 API,也是继续理解所有权、借用、迭代器和集合操作的基础。

1. String

1.1 概述

String 是标准库提供的可增长字符串类型,内容使用 UTF-8 编码。

它和字符串字面量 &str 的区别是:

  • String 拥有字符串内容,可以增长、修改、移动。
  • &str 是字符串切片,只是借用一段 UTF-8 字符串内容。

1.2 创建字符串

功能:创建一个新的 String

常用方式

#![allow(unused)]
fn main() {
let mut s1 = String::new();
let s2 = String::from("hello");
let s3 = "world".to_string();
}
  • String::new():创建空字符串。
  • String::from(...):从字符串字面量创建。
  • .to_string():把实现了 Display 的值转换成字符串。

1.3 追加内容

功能:向字符串末尾追加内容。

接口签名

#![allow(unused)]
fn main() {
pub fn push_str(&mut self, string: &str)
pub fn push(&mut self, ch: char)
}
  • push_str 追加字符串切片。
  • push 追加单个字符。

简单调用示例

#![allow(unused)]
fn main() {
let mut s = String::from("Rust");
s.push(' ');
s.push_str("book");
println!("{}", s);
}

1.4 遍历字符串

Rust 字符串是 UTF-8,不能随便按字节索引字符。

常见遍历方式:

#![allow(unused)]
fn main() {
let s = String::from("你好 Rust");

for ch in s.chars() {
    println!("{}", ch);
}

for byte in s.bytes() {
    println!("{}", byte);
}
}
  • chars():按 Unicode 标量值遍历。
  • bytes():按 UTF-8 字节遍历。

1.5 解析字符串

功能:把字符串解析成目标类型。

接口签名

#![allow(unused)]
fn main() {
pub fn parse<F>(&self) -> Result<F, F::Err>
where
    F: FromStr
}

简单调用示例

#![allow(unused)]
fn main() {
let number = "42".parse::<i32>().unwrap();
println!("{}", number);
}

parse 返回 Result,因为字符串内容不一定能成功解析成目标类型。

2. Vec<T>

2.1 概述

Vec<T> 是动态数组,所有元素类型必须相同。它把元素放在连续内存中,因此按索引访问很快。

适合场景:

  • 按顺序存储一组元素。
  • 需要尾部追加。
  • 需要通过索引读取元素。

2.2 创建 Vec

常用方式

#![allow(unused)]
fn main() {
let mut v1 = Vec::<i32>::new();
let v2 = vec![1, 2, 3];
}
  • Vec::new():创建空 Vec。
  • vec![]:用宏直接创建带初始元素的 Vec。

2.3 添加元素

接口签名

#![allow(unused)]
fn main() {
pub fn push(&mut self, value: T)
}

简单调用示例

#![allow(unused)]
fn main() {
let mut numbers = Vec::new();
numbers.push(10);
numbers.push(20);
}

push 会把元素追加到尾部。如果容量不够,Vec 会重新分配更大的内存。

2.4 读取元素

常见方式:

#![allow(unused)]
fn main() {
let numbers = vec![10, 20, 30];

let first = numbers[0];
let maybe = numbers.get(10);
}
  • numbers[index]:越界会 panic
  • numbers.get(index):返回 Option<&T>,越界返回 None

2.5 删除元素

常用接口:

#![allow(unused)]
fn main() {
pub fn pop(&mut self) -> Option<T>
pub fn remove(&mut self, index: usize) -> T
}
  • pop 从尾部删除,返回 Option<T>
  • remove 删除指定位置元素,并移动后续元素填补空位。

简单调用示例

#![allow(unused)]
fn main() {
let mut numbers = vec![10, 20, 30];
let last = numbers.pop();
let first = numbers.remove(0);
}

3. HashMap<K, V>

3.1 概述

HashMap<K, V> 是键值映射,适合通过 key 快速查找 value。

使用 HashMap 时,key 需要实现:

  • Eq:能判断两个 key 是否相等。
  • Hash:能计算哈希值。

3.2 创建 HashMap

#![allow(unused)]
fn main() {
use std::collections::HashMap;

let mut scores = HashMap::<String, i32>::new();
}

3.3 插入元素

接口签名

#![allow(unused)]
fn main() {
pub fn insert(&mut self, k: K, v: V) -> Option<V>
}
  • 如果 key 不存在,插入新键值对,返回 None
  • 如果 key 已存在,覆盖旧值,返回 Some(old_value)

简单调用示例

#![allow(unused)]
fn main() {
scores.insert(String::from("Alice"), 95);
scores.insert(String::from("Bob"), 82);
}

3.4 查找元素

接口签名

#![allow(unused)]
fn main() {
pub fn get<Q>(&self, k: &Q) -> Option<&V>
where
    K: Borrow<Q>,
    Q: Hash + Eq + ?Sized
}

简单调用示例

#![allow(unused)]
fn main() {
if let Some(score) = scores.get("Alice") {
    println!("{}", score);
}
}

get 返回 Option<&V>,因为 key 可能不存在。

3.5 entry API

entry 适合“如果没有就插入,如果有就修改”的场景。

#![allow(unused)]
fn main() {
scores.entry(String::from("Carol")).or_insert(90);
*scores.entry(String::from("Alice")).or_insert(0) += 1;
}

3.6 遍历和删除

#![allow(unused)]
fn main() {
for (key, value) in &scores {
    println!("{} => {}", key, value);
}

scores.remove("Bob");
}

注意:HashMap 的遍历顺序不稳定,不要依赖输出顺序。

4. VecDeque<T>

4.1 概述

VecDeque<T> 是双端队列,支持从头部和尾部高效插入/删除。

它适合:

  • 队列:从尾部 push_back,从头部 pop_front
  • 双端任务列表:头尾都可能插入或删除。

VecDeque 内部使用环形缓冲区,不保证所有元素在内存中完全连续。

4.2 创建 VecDeque

#![allow(unused)]
fn main() {
use std::collections::VecDeque;

let mut queue = VecDeque::<String>::new();
}

4.3 头尾插入

接口签名

#![allow(unused)]
fn main() {
pub fn push_back(&mut self, value: T)
pub fn push_front(&mut self, value: T)
}

简单调用示例

#![allow(unused)]
fn main() {
queue.push_back(String::from("task-1"));
queue.push_front(String::from("urgent-task"));
}

4.4 头尾删除

接口签名

#![allow(unused)]
fn main() {
pub fn pop_front(&mut self) -> Option<T>
pub fn pop_back(&mut self) -> Option<T>
}

简单调用示例

#![allow(unused)]
fn main() {
while let Some(task) = queue.pop_front() {
    println!("handle {}", task);
}
}

5. 迭代器

5.1 为什么需要迭代器?

前四节我们分别介绍了 StringVecHashMapVecDeque。它们结构不同,但有一个共同需求:遍历元素。如果每种容器都用自己的方式遍历,代码就会变成这样:

#![allow(unused)]
fn main() {
// 不同容器,不同遍历方式——很割裂
for i in 0..v.len()       { let x = v[i]; }      // Vec: 手动索引
for (k, v) in &map        { /* ... */ }           // HashMap: 解构键值对
while let Some(x) = q.pop_front() { /* ... */ }   // VecDeque: 消费式遍历
}

Rust 的做法是:所有容器统一通过迭代器提供遍历能力。迭代器是一个标准化的抽象层——无论容器内部是什么数据结构,对外都暴露同一套操作接口(mapfiltercollect 等)。

5.2 Iterator trait

迭代器的核心是 Iterator trait:

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;                        // 迭代产出的元素类型
    fn next(&mut self) -> Option<Self::Item>;  // 核心方法:取下一个元素
    // ... 其余 70+ 个方法都有默认实现
}
}

只需要实现 next,其余所有方法(mapfiltercollectfold 等)自动获得。

next 的返回值表达了迭代器的状态:

  • Some(item) → 还有元素,返回下一个。
  • None → 迭代结束,停止。

5.3 三种迭代形式

Rust 容器对“遍历“这件事有三种不同的权限模型:

方法产出类型所有权使用场景
.iter()&T借用,不消费容器只读遍历,之后还能用容器
.iter_mut()&mut T可变借用,不消费容器遍历中修改元素
.into_iter()T消费容器,转移所有权遍历后不再需要原容器
#![allow(unused)]
fn main() {
let mut v = vec![1, 2, 3];

// 只读遍历——容器原样保留
for x in v.iter() {
    println!("{}", x);
}
println!("容器还在: {:?}", v);   // OK

// 可变遍历——可以修改元素
for x in v.iter_mut() {
    *x *= 10;
}
println!("元素翻倍: {:?}", v);   // [10, 20, 30]

// 消费式遍历——所有权转移,原容器不可再用
let sum: i32 = v.into_iter().sum();
println!("{}", sum);
// println!("{:?}", v);          // 编译错误:v 已被消费
}

for 循环的语法糖for x in &v 等价于 for x in v.iter()for x in &mut v 等价于 for x in v.iter_mut()for x in v 等价于 for x in v.into_iter()for 循环会自动调用对应的迭代器方法,写起来更简洁。

5.4 迭代器适配器(adaptor)

迭代器的真正威力在于链式组合。适配器接收一个迭代器,返回一个新的迭代器——零拷贝的流水线处理:

#![allow(unused)]
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];

// 链式处理:每个适配器返回一个新的迭代器
let result: Vec<i32> = numbers
    .iter()                 // 产生 &i32
    .filter(|&&x| x % 2 == 0)   // 只保留偶数
    .map(|&x| x * 10)      // 每个元素乘以 10
    .collect();             // 收集到 Vec 中

println!("{:?}", result);   // [20, 40, 60]
}

惰性求值:迭代器适配器不会立即执行——只有当你调用 collect()sum()for 循环等消费器(consumer)时,整个流水线才开始计算。这意味着中间步骤不分配临时 Vec

常用适配器一览:

适配器作用示例
.map(|x| ...)转换每个元素.map(|x| x * 2)
.filter(|x| ...)保留满足条件的元素.filter(|x| *x > 0)
.filter_map(|x| ...)过滤 + 转换一步完成.filter_map(|x| x.parse().ok())
.take(n)只取前 n 个.take(5)
.skip(n)跳过前 n 个.skip(1)
.enumerate()附加索引 (index, item).enumerate()
.chain(other)拼接两个迭代器.chain(b.iter())
.zip(other)成对组合.zip(b.iter())
.rev()反向遍历(仅双端迭代器).rev()

5.5 消费器(consumer)

消费器触发流水线执行,并产出最终结果:

#![allow(unused)]
fn main() {
let v = vec![1, 2, 3, 4, 5];

// collect: 收集到任意集合类型
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
let doubled_set: std::collections::HashSet<i32> = v.iter().map(|x| x * 2).collect();

// fold: 带初始值的累加
let sum = v.iter().fold(0, |acc, x| acc + x);       // 15

// reduce: 无初始值的累加(返回 Option)
let product = v.iter().copied().reduce(|a, b| a * b); // Some(120)

// sum / product: 内置的求和/求积
let total: i32 = v.iter().sum();                      // 15

// any / all: 条件判断
let has_even = v.iter().any(|x| x % 2 == 0);          // true
let all_positive = v.iter().all(|x| *x > 0);          // true

// count: 计数
let n = v.iter().filter(|x| **x > 2).count();         // 3

// find / position: 查找
let first = v.iter().find(|x| **x > 3);               // Some(&4)
let idx = v.iter().position(|x| *x == 3);              // Some(2)

// max / min: 极值
let max = v.iter().max();                              // Some(&5)
let min = v.iter().min();                              // Some(&1)
}

5.6 和四种容器的配合

迭代器与本章四种容器的常见用法:

String

#![allow(unused)]
fn main() {
let s = String::from("Rust");
let upper: String = s.chars().map(|c| c.to_ascii_uppercase()).collect();
// chars() 返回 chars 迭代器,collect() 收集回 String
}

Vec

#![allow(unused)]
fn main() {
let words = vec!["hello", "world"];
let lengths: Vec<usize> = words.iter().map(|w| w.len()).collect();
}

HashMap

#![allow(unused)]
fn main() {
use std::collections::HashMap;
let scores = HashMap::from([("Alice", 95), ("Bob", 82)]);
let names: Vec<&str> = scores.keys().copied().collect();
let high_scores: HashMap<_, _> = scores
    .into_iter()
    .filter(|(_, v)| *v > 80)
    .collect();
}

VecDeque

#![allow(unused)]
fn main() {
use std::collections::VecDeque;
let q = VecDeque::from([1, 2, 3]);
let doubled: VecDeque<i32> = q.iter().map(|x| x * 2).collect();
}

5.7 迭代器的性能

Rust 迭代器是零成本抽象——经过编译器优化后,链式迭代器调用往往被内联展开成与手写 for 循环等效甚至更优的机器码。你可以放心使用迭代器,不用为“抽象开销“担心。

举个例子,下面两段代码生成的机器码几乎一样:

#![allow(unused)]
fn main() {
// 迭代器版本
let sum: i32 = v.iter().filter(|x| **x > 0).map(|x| x * 2).sum();

// 手写循环版本
let mut sum = 0;
for x in &v {
    if *x > 0 {
        sum += x * 2;
    }
}
}

迭代器版本可读性更好,性能没有损失——这正是 Rust “无代价抽象” 哲学的体现。


6. 综合示例:任务看板

这个示例把四种容器放在一起使用:

  • String 保存原始输入。
  • VecDeque<String> 保存待处理任务队列。
  • HashMap<String, String> 保存任务负责人。
  • Vec<String> 保存已完成任务。
#![allow(unused)]
fn main() {
use std::collections::{HashMap, VecDeque};

pub fn demo() {
    let raw_tasks = String::from("learn rust,write demo,read book");
    let mut waiting = VecDeque::<String>::new();
    let mut finished = Vec::<String>::new();
    let mut owners = HashMap::<String, String>::new();

    for task in raw_tasks.split(',') {
        waiting.push_back(task.trim().to_string());
    }

    owners.insert(String::from("learn rust"), String::from("Alice"));
    owners.insert(String::from("write demo"), String::from("Bob"));
    owners.insert(String::from("read book"), String::from("Carol"));

    while let Some(task) = waiting.pop_front() {
        let owner = owners
            .get(&task)
            .map(String::as_str)
            .unwrap_or("unknown");
        println!("{} is handling {}", owner, task);
        finished.push(task);
    }

    println!("finished tasks = {:?}", finished);
}
}