容器 ------ Vec、HashMap、Index、Iterator
一、C 中的"容器"------没有标准容器库
1.1 C 只有数组,没有容器
C 标准库提供的数据结构只有数组------原始、固定大小、不安全:
c
// C 的数组------最基本的容器
int arr[10]; // 固定大小,编译时确定
arr[0] = 42; // 访问元素
int x = arr[10]; // ❌ 越界!编译器不报错
C 中没有 std::vector、没有 std::map、没有 std::set。如果要动态数组,必须手写:
c
// C 中的"动态数组"------手写
struct DynArray {
int* data;
size_t size; // 有效元素个数
size_t capacity; // 已分配容量
};
struct DynArray* da_new() {
struct DynArray* da = malloc(sizeof(*da));
da->data = malloc(4 * sizeof(int));
da->size = 0;
da->capacity = 4;
return da;
}
void da_push(struct DynArray* da, int val) {
if (da->size >= da->capacity) {
da->capacity *= 2;
da->data = realloc(da->data, da->capacity * sizeof(int));
}
da->data[da->size++] = val;
}
void da_free(struct DynArray* da) {
free(da->data);
free(da);
}
每次写一个新项目,都要重新写一遍这个模式。 不同的项目可能有不同的命名------DynArray、ArrayList、Vector------没有统一的标准。
1.2 C 没有标准哈希表
c
// C 中没有 std::unordered_map
// 如果你想用哈希表:
// 选项 1:手写一个(严重的工程负担)
// 选项 2:使用第三方库(uthash、glib GHashTable)
// 选项 3:用 POSIX hsearch(功能非常有限)
// uthash 的例子------需要宏,没有类型安全
#include <uthash.h>
struct User {
int id;
char name[32];
UT_hash_handle hh; // 宏需要的内部字段
};
struct User* users = NULL;
struct User* u = malloc(sizeof(*u));
u->id = 42;
strcpy(u->name, "Alice");
HASH_ADD_INT(users, id, u); // 宏
C 没有标准化的哈希表。 每个项目要么手写、要么选择第三方库、要么接受 POSIX 有限的 hsearch。
1.3 C 中"容器"的问题总结
| 问题 | 说明 |
|---|---|
| 没有统一容器库 | 动态数组、哈希表、集合都没有标准实现 |
| 每次都要手写 | 每个项目自己实现一套,命名不同、接口不同 |
| 类型不安全 | void* 是最常见的"泛型"方式 |
| 内存管理手动的 | 扩容、收缩、释放------全部手动 |
| 没有迭代器协议 | 遍历方式各不相同(索引、指针、回调) |
| 没有统一的访问语法 | arr[i] 只对原生数组有效 |
二、Vec<T> ------ 动态数组
2.1 基本用法
Vec<T> 是 Rust 中最常用的容器,相当于 C 手写的动态数组------但类型安全、自动管理内存:
rust
// 创建
let mut v: Vec<i32> = Vec::new(); // 空 Vec
let mut v = vec![1, 2, 3]; // 宏:用初始值创建
// 增删
v.push(4); // 尾部追加
v.pop(); // 尾部弹出(返回 Option<i32>)
v.insert(1, 100); // 指定位置插入
v.remove(0); // 指定位置删除
// 访问
let x = v[0]; // 通过 Index trait(越界 panic)
let x = v.get(0); // 安全的访问(返回 Option<&T>)
// 信息
println!("len: {}, cap: {}", v.len(), v.capacity());
// Vec 自动管理:满了就加倍扩容(类似 C 的 realloc,但自动)
2.2 Vec 的内存管理
rust
fn vec_memory() {
let mut v: Vec<i32> = Vec::with_capacity(4);
// v: 容量 = 4, 长度 = 0
// 堆上已分配可以容纳 4 个 i32 的空间
v.push(10); // 长度 = 1,不需要扩容
v.push(20); // 长度 = 2
v.push(30); // 长度 = 3
v.push(40); // 长度 = 4
v.push(50); // 长度 = 5 > 容量 = 4 → 自动扩容!
// 内部:分配新内存(容量 = 8),复制旧元素,释放旧内存
// 所有元素被 move 到新位置
// v 离开作用域 → Vec::drop() 自动释放堆内存
// 不需要手动 free
} // ✅ 自动释放
rust
Vec 的内存布局:
栈上: 堆上:
┌──────────────┐ ┌────┬────┬────┬────┬────┬────┬────┬────┐
│ Vec<i32> │ │ 10 │ 20 │ 30 │ 40 │ 50 │ │ │ │
│ ptr ─────────▶ ├────┼────┼────┼────┼────┼────┼────┼────┤
│ len: 5 │ │ ←─── 已使用 ──→│←─── 未使用 ──→│
│ cap: 8 │ └───────────────────────────────────────┘
└──────────────┘
对比 C 的手写动态数组------Rust 的 Vec 内部做的是一样的事(malloc / realloc / free),区别在于:
| 维度 | C 手写动态数组 | Rust Vec<T> |
|---|---|---|
| 初始化 | 手动 malloc + 初始化字段 | Vec::new() 或 vec![] |
| 扩容 | 手动 realloc + 更新容量 | push() 自动扩容 |
| 释放 | 手动 free | 离开作用域自动 Drop |
| 越界访问 | arr[i] 不检查 |
v[i] 检查边界(越界 panic) |
| 类型安全 | void* 或具体类型 |
Vec<T> 泛型------类型保留 |
2.3 Vec 的底层就是 C 的动态数组
rust
// Vec 的内部结构大致等价于 C 的:
// struct Vec<T> {
// ptr: *mut T, // 指向堆上分配的连续内存
// len: usize, // 已使用的元素个数
// cap: usize, // 已分配的元素容量
// }
Vec 不是魔法------它和你手写的 C 动态数组做同样的事。 只是它把这些事封装得安全、自动、不遗忘。
rust
// 获得底层裸指针(与 C 交互时用)
let mut v = vec![1, 2, 3];
let ptr = v.as_mut_ptr(); // *mut i32,和 C 的 int* 一样
let len = v.len();
let cap = v.capacity();
// 可以传给 C 函数
// unsafe { c_function(ptr, len); }
2.4 Vec 提供的安全保证
rust
// 1. 越界检查(debug 模式 panic,release 模式可优化掉)
let v = vec![1, 2, 3];
// v[100]; // ❌ panic: index out of bounds
// 2. 永远不会 double-free
// Vec 是 Drop 的,离开作用域自动释放
// 所有权 move 后原 Vec 失效
// 3. 不会使用已释放的内存
let v = vec![1, 2, 3];
let ptr = v.as_ptr();
drop(v); // 显式释放
// println!("{}", ptr); // ❌ 编译错误------v 已移出
三、HashMap<K, V> ------ 哈希表
3.1 基本用法
rust
use std::collections::HashMap;
let mut scores = HashMap::new();
// 插入
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// 访问
let team = String::from("Blue");
let score = scores.get(&team); // ✅ Option<&i32>
// scores["Blue"]; // ✅ 通过 Index------但可能 panic(key 不存在)
// 遍历
for (key, value) in &scores {
println!("{}: {}", key, value);
}
// 安全更新
scores.entry(String::from("Blue"))
.or_insert(0); // 如果不存在才插入
3.2 C 中没有标准哈希表
c
// C 中如果要哈希表:
// 选项 1:手写(几百行代码,容易有 bug)
// 选项 2:uthash(宏实现,语法不直观)
// 选项 3:glib(外部依赖)
// uthash 的例子:
#include <uthash.h>
struct User {
int id;
char name[32];
UT_hash_handle hh;
};
struct User* users = NULL;
// 插入
struct User* u = malloc(sizeof(*u));
u->id = 42;
strcpy(u->name, "Alice");
HASH_ADD_INT(users, id, u); // 宏------不是函数,没有类型检查
// 查找
struct User* found;
HASH_FIND_INT(users, &id, found); // found 可能是 NULL
3.3 Rust 的 HashMap 对比 C
| 维度 | C 哈希表(手写/uthash) | Rust HashMap<K, V> |
|---|---|---|
| 类型安全 | 无(void* 或宏) |
有(HashMap<String, i32>) |
| 标准库提供 | 否 | 是 |
| 插入语法 | HASH_ADD_INT(宏) |
.insert(key, val) |
| 查找语法 | HASH_FIND_INT(宏) |
.get(&key) 返回 Option<&V> |
| 缺失 key 的处理 | 返回 NULL(类型不安全) | 返回 None(类型安全) |
| 遍历 | 手写循环 | for (k, v) in &map |
| 内存管理 | 手动 free 每个 entry | 容器 Drop 时自动释放 |
四、Index trait ------ v[i] 的背后
4.1 C 的 arr[i] 只是指针运算
c
int arr[10];
arr[3] = 42; // 等价于 *(arr + 3)
// C 的 arr[i] 只对原生数组和指针有效
// 对自定义类型(如上面手写的 DynArray)不能用 []
struct DynArray* da = da_new();
// da[0] = 42; // ❌ 不能这样用------DynArray 不是数组
// 只能用函数:da_set(da, 0, 42);
4.2 Rust 的 Index trait
Rust 用 Index trait 让 v[i] 语法适用于所有容器:
rust
// Index trait 的定义
pub trait Index<Idx> {
type Output: ?Sized;
fn index(&self, index: Idx) -> &Self::Output;
}
// IndexMut trait(允许写入)
pub trait IndexMut<Idx>: Index<Idx> {
fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}
rust
let v = vec![10, 20, 30];
// v[1] 实际上展开为:
// *v.index(1) ← 调用 Index trait 的 index 方法
// 所以:
println!("{}", v[1]); // 20 → 自动调用 v.index(1)
println!("{}", v.index(1)); // 20 → 显式调用,和上面等价
4.3 Vec 实现了 Index
rust
// Vec<i32> 实现了 Index<usize>
// 所以 v[i] 对 usize 有效
let v = vec![1, 2, 3];
let x = v[1]; // 2
// 边界检查在哪里?
// 在 Index 的实现里------Vec::index() 内部
// pub fn index(&self, index: usize) -> &T {
// assert!(index < self.len()); // ❌ 越界 panic
// unsafe { self.get_unchecked(index) }
// }
4.4 String 也实现了 Index
rust
let s = String::from("hello");
// String 实现了 Index<Range<usize>>
let sub = &s[0..2]; // "he"------切片
let sub = &s[1..4]; // "ell"
// 但 String 没有实现 Index<usize>
// let c = s[0]; // ❌ 编译错误
// 因为 Rust 的 String 是 UTF-8 编码,一个"字符"不一定是一字节
// s.bytes() → 按字节迭代
// s.chars() → 按 Unicode 标量值迭代
4.5 HashMap 实现了 Index
rust
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("key".to_string(), 42);
let val = map["key"]; // ✅ 通过 Index 访问
// 等价于:*map.index(&"key".to_string())
// let val = map["nope"]; // ❌ panic: no entry found for key
// HashMap 的 Index 实现在 key 不存在时会 panic!
// 安全的做法用 .get():
match map.get("nope") {
Some(v) => println!("{}", v),
None => println!("not found"),
}
4.6 Index 让自定义容器也支持 []
rust
// 自定义容器------通过实现 Index 支持 [] 语法
struct MyVec<T> {
data: Vec<T>,
}
impl<T> Index<usize> for MyVec<T> {
type Output = T;
fn index(&self, index: usize) -> &T {
&self.data[index] // 委托给内部 Vec
}
}
let mv = MyVec { data: vec![1, 2, 3] };
println!("{}", mv[1]); // ✅ 2------通过自定义 Index 实现
Index trait 的意义: 让 [] 不再是语言内置的语法,而是可以被任何类型实现的 trait。C 中只能 arr[i],Rust 中任何容器都可以 container[i]。
五、Iterator trait ------ for x in v 的背后
5.1 C 的遍历------每种容器不同
c
// C 的三种常见遍历方式:
// 方式 1:索引(对数组)
int arr[10];
for (int i = 0; i < 10; i++) {
process(arr[i]);
}
// 方式 2:指针(对数组)
for (int* p = arr; p < arr + 10; p++) {
process(*p);
}
// 方式 3:链表遍历
struct Node* head = /* ... */;
for (struct Node* p = head; p != NULL; p = p->next) {
process(p->data);
}
// 方式 4:回调(对某些库)
hashmap_foreach(map, process); // 传入函数指针
每种容器的遍历方式都不同。 没有一个统一的"遍历"接口。
5.2 Rust 的 Iterator trait
rust
// Iterator trait 的核心定义
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
任何实现了 Iterator 的类型都可以用 for 循环遍历:
rust
let v = vec![1, 2, 3];
// for 循环遍历
for x in &v { // &v 实现了 IntoIterator
println!("{}", x);
}
// for 循环实际上展开为:
let mut iter = (&v).into_iter();
while let Some(x) = iter.next() {
println!("{}", x);
}
5.3 IntoIterator ------ for 循环的入口
rust
// for 循环要求类型实现 IntoIterator
pub trait IntoIterator {
type Item;
type IntoIter: Iterator<Item = Self::Item>;
fn into_iter(self) -> Self::IntoIter;
}
Vec 的三种迭代方式:
rust
let v = vec![1, 2, 3];
// 1. for x in &v ------ 迭代不可变引用(最常用)
for x in &v {
// x: &i32,只能读
}
// 2. for x in &mut v ------ 迭代可变引用
for x in &mut v {
// x: &mut i32,可以写
*x *= 2;
}
// 3. for x in v ------ 消费 Vec(所有权)
for x in v {
// x: i32,v 的所有权已转移,之后不能再用 v
}
5.4 迭代器适配器
Iterator trait 提供了大量方法,可以对序列进行链式操作:
rust
let v = vec![1, 2, 3, 4, 5];
// 链式操作------编译器优化后和手写循环一样高效
let result: Vec<i32> = v.iter()
.filter(|x| *x % 2 == 0) // 只保留偶数
.map(|x| x * 2) // 每个乘以 2
.collect(); // 收集到 Vec
println!("{:?}", result); // [4, 8]
// 注意:这是"零成本抽象"------适配器链在编译期被合成为等价的手写循环
// 没有运行时开销
常用适配器:
rust
let v = vec![4, 2, 5, 1, 3];
// filter ------ 过滤
v.iter().filter(|x| **x > 2); // 只保留 > 2 的元素
// map ------ 变换
v.iter().map(|x| x * 2); // 每个元素乘以 2
// take ------ 取前 n 个
v.iter().take(3); // 只取前 3 个
// skip ------ 跳过 n 个
v.iter().skip(2); // 跳过前 2 个
// chain ------ 拼接
v.iter().chain([10, 11].iter()); // 先 v 再 [10, 11]
// enumerate ------ 带索引
v.iter().enumerate(); // (index, value) 对
// zip ------ 配对
v.iter().zip(["a", "b", "c"].iter()); // (1,"a"), (2,"b"), (3,"c")
// fold ------ 折叠
v.iter().fold(0, |acc, x| acc + x); // 求和
// all / any ------ 检查
v.iter().all(|x| x > 0); // 所有元素 > 0?
v.iter().any(|x| x == 3); // 有元素 == 3?
// find ------ 查找
v.iter().find(|x| **x == 3); // 返回第一个等于 3 的元素
// position ------ 找位置
v.iter().position(|x| *x == 3); // 返回 3 的位置(索引)
5.5 迭代器是惰性的
rust
let v = vec![1, 2, 3, 4, 5];
// 这段代码什么都不做------迭代器是惰性的
let iter = v.iter()
.filter(|x| { println!("filter {}", x); **x % 2 == 0 })
.map(|x| { println!("map {}", x); x * 2 });
// 只有调用 .collect() 或 .for_each() 等"消费者"时,才真正执行
let result: Vec<_> = iter.collect(); // 这里才开始计算
// 输出:
// filter 1
// filter 2
// map 2
// filter 3
// filter 4
// map 4
// filter 5
5.6 C 没有等效的迭代器体系
c
// C 中实现"链式操作":
int arr[] = {1, 2, 3, 4, 5};
int n = 5;
// 步骤 1:过滤偶数
int temp1[5];
int cnt1 = 0;
for (int i = 0; i < n; i++) {
if (arr[i] % 2 == 0) {
temp1[cnt1++] = arr[i];
}
}
// 步骤 2:每个乘以 2
int temp2[5];
for (int i = 0; i < cnt1; i++) {
temp2[i] = temp1[i] * 2;
}
// ...... 每个操作都需要一个临时数组、一个循环
// 代码增长了,引入了临时变量,容易出错
rust
// Rust ------ 链式操作,无需临时变量
let result: Vec<_> = arr.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * 2)
.collect();
六、C vs Rust 容器对比
6.1 容器类型对比
| 容器类型 | C | Rust |
|---------|---|------| | 动态数组 | 手写(malloc/realloc/free) | Vec<T> | | 哈希表 | 无标准(uthash/glib) | HashMap<K, V> | | 有序哈希表 | 无 | BTreeMap<K, V> | | 集合 | 无标准 | HashSet<T> / BTreeSet<T> | | 双端队列 | 手写 | VecDeque<T> | | 链表 | 手写 | LinkedList<T> | | 字符串 | char* + <string.h> | String / &str |
6.2 接口对比
| 操作 | C(手写动态数组) | Rust Vec<T> |
|---|---|---|
| 创建 | struct DynArray* da = da_new() |
let v = Vec::new() |
| 追加 | da_push(da, val) |
v.push(val) |
| 删除末尾 | 需要手写 | v.pop() |
| 获取元素 | da_get(da, i) 或 da->data[i] |
v[i](Index trait) |
| 获取长度 | da->size |
v.len() |
| 遍历 | for (int i=0; i<da->size; i++) |
for x in &v(Iterator) |
| 排序 | 手写或 qsort |
v.sort() |
| 释放 | da_free(da) |
自动 Drop |
6.3 一个完整的功能:C vs Rust
c
// C ------ 用动态数组存用户数据,遍历、过滤
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct User {
int id;
char name[64];
};
struct DynArray {
struct User* data;
size_t size, capacity;
};
struct DynArray* da_new() { /* ... */ }
void da_push(struct DynArray* da, struct User u) { /* ... */ }
void da_free(struct DynArray* da) { /* ... */ }
int main() {
struct DynArray* users = da_new();
struct User u1 = {1, "Alice"};
struct User u2 = {2, "Bob"};
struct User u3 = {3, "Charlie"};
da_push(users, u1);
da_push(users, u2);
da_push(users, u3);
// 过滤 id > 1 的用户并打印
for (size_t i = 0; i < users->size; i++) {
if (users->data[i].id > 1) {
printf("%s\n", users->data[i].name);
}
}
da_free(users);
return 0;
}
rust
// Rust ------ 同样的功能
#[derive(Debug)]
struct User {
id: i32,
name: String,
}
fn main() {
let users = vec![
User { id: 1, name: "Alice".into() },
User { id: 2, name: "Bob".into() },
User { id: 3, name: "Charlie".into() },
];
// 过滤 id > 1 的用户并打印
users.iter()
.filter(|u| u.id > 1)
.for_each(|u| println!("{}", u.name));
// users 离开作用域 → 自动释放 ✅
}
七、与 C 程序员的对话
"Vec 不就是封装了 realloc 吗?"
C 程序员:"Vec 的 push 背后就是 realloc------我手写的动态数组也是这么做的。Vec 只是帮我自动调用了 realloc 而已。"
Rust :"这就是关键------'自动调用'意味着你永远不需要记得调用它。你永远不需要检查 realloc 是否返回 NULL。你永远不需要在不用的时候手动 free。而且v[i]有边界检查------C 的arr[i]没有。这些'自动'加起来,就是消除了一整个类别的内存 bug。"
c
// C ------ 忘了一个检查就是 bug
int* p = realloc(arr, new_size * sizeof(int));
if (p == NULL) { /* 必须检查 */ } // 如果忘了------灾难
rust
// Rust ------ Vec 内部处理 OOM(通常 panic)
// 你不需要写检查代码
let mut v = vec![1, 2, 3];
v.push(4); // ✅ 自动扩容,自动处理错误
"哈希表我用了 uthash 很久了,没什么不好"
C 程序员:"uthash 用宏实现的哈希表,我用了很多年,没什么问题。"
Rust :"uthash 是一个优秀的库------但它不是标准的一部分,每个 C 项目需要自己决定用不用、用哪个。而且宏实现的语法不是 Rust 式的函数调用------HASH_ADD_INT(users, id, u)------类型检查有限,宏展开出错难以调试。Rust 的HashMap是标准库的一部分,.调用的语法在任何编辑器里都有自动补全。"
c
// C------uthash 的宏
HASH_FIND_INT(users, &id, found);
// 宏:不检查 users 的类型、id 的类型、found 的类型
rust
// Rust------方法是类型安全的
let found: Option<&User> = users.get(&id);
// 如果 id 的类型和 key 类型不匹配------编译错误
"Iterator 不就是函数指针回调吗?"
C 程序员 :"Rust 的
for x in &v让我想起 C 中函数指针的回调遍历,只是语法不一样。"
Rust :"不只是语法。迭代器是惰性的------你可以构建一个操作链(.filter().map()),只有最后调用.collect()时才真正执行。C 的函数回调做不到这种惰性组合------每个回调都是立即执行的。而且迭代器适配器链在编译期会被优化成手写循环------零运行时开销。"
c
// C ------ 立即执行,不能链式组合
void process_if_even(void* ctx, int val) {
if (val % 2 == 0) {
process(val * 2);
}
}
foreach(arr, n, process_if_even); // 现在就执行了
rust
// Rust------惰性组合
let result: Vec<_> = arr.iter()
.filter(|x| *x % 2 == 0) // 只是声明了"要过滤"
.map(|x| x * 2) // 只是声明了"要变换"
.collect(); // 到这里才真正执行
八、小结
8.1 C 的问题
css
C 没有标准容器库
→ 动态数组、哈希表、集合都需要手写
→ 每种容器的遍历方式不同
→ 没有统一的访问语法([] 只对原生数组有效)
→ 内存管理全部手动
8.2 Rust 的解决
| 问题 | Rust 的解决 |
|---|---|
| 没有标准动态数组 | Vec<T>(类型安全、自动扩容、自动释放) |
| 没有标准哈希表 | HashMap<K, V>(标准库提供) |
| 没有统一访问语法 | Index trait------任何容器都支持 container[i] |
| 没有统一遍历方式 | Iterator trait------所有容器都用 for x in &container |
| 内存管理手动 | 容器类型实现 Drop------离开作用域自动释放 |
8.3 容器 + Trait 的关系
rust
容器(Vec、HashMap、String......)
↓ 实现了
Index trait → container[i] 语法
IntoIterator trait → for x in container 语法
↓
调用者不需要知道容器内部是什么
只需要知道:它实现了 Index + IntoIterator
一句话总结 :C 没有标准容器库------动态数组手写、哈希表第三方、遍历方式各不相同。Rust 的标准容器(
Vec、HashMap等)通过Indextrait(统一的[]访问)和Iteratortrait(统一的for遍历)与语言语法深度集成------让"操作容器"和"操作原生数组"一样自然,同时保持类型安全和内存安全。