从C到Rust:容器,容器,容器,Index和Iterator这两口糖甜不甜?

2 阅读14分钟

容器 —— Vec、HashMap、Index、Iterator

一、C 中的"容器"——没有标准容器库

1.1 C 只有数组,没有容器

C 标准库提供的数据结构只有数组——原始、固定大小、不安全:


// C 的数组——最基本的容器

int arr[10]; // 固定大小,编译时确定

arr[0] = 42; // 访问元素

int x = arr[10]; // ❌ 越界!编译器不报错

C 中没有 std::vector、没有 std::map、没有 std::set。如果要动态数组,必须手写:


// 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);

}

每次写一个新项目,都要重新写一遍这个模式。 不同的项目可能有不同的命名——DynArrayArrayListVector——没有统一的标准。

1.2 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 手写的动态数组——但类型安全、自动管理内存:


// 创建

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 的内存管理


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

} // ✅ 自动释放


Vec 的内存布局:

  


栈上:             堆上:

┌──────────────┐ ┌────┬────┬────┬────┬────┬────┬────┬────┐
│ Vec<i32>     │ │ 1020304050 │    │    │    │
│ 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 的动态数组


// Vec 的内部结构大致等价于 C 的:

// struct Vec<T> {

// ptr: *mut T, // 指向堆上分配的连续内存

// len: usize, // 已使用的元素个数

// cap: usize, // 已分配的元素容量

// }

Vec 不是魔法——它和你手写的 C 动态数组做同样的事。 只是它把这些事封装得安全、自动、不遗忘。


// 获得底层裸指针(与 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 提供的安全保证


// 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 基本用法


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 中如果要哈希表:

// 选项 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] 只是指针运算


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] 语法适用于所有容器:


// 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;

}


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


// 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


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


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 让自定义容器也支持 []


// 自定义容器——通过实现 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 的三种常见遍历方式:

  


// 方式 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


// Iterator trait 的核心定义

pub trait Iterator {

type Item;

fn next(&mut self) -> Option<Self::Item>;

}

任何实现了 Iterator 的类型都可以用 for 循环遍历:


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 循环的入口


// for 循环要求类型实现 IntoIterator

pub trait IntoIterator {

type Item;

type IntoIter: Iterator<Item = Self::Item>;

fn into_iter(self) -> Self::IntoIter;

}

Vec 的三种迭代方式:


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 提供了大量方法,可以对序列进行链式操作:


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]

  


// 注意:这是"零成本抽象"——适配器链在编译期被合成为等价的手写循环

// 没有运行时开销

常用适配器:


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 迭代器是惰性的


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 中实现"链式操作":

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 —— 链式操作,无需临时变量

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->sizev.len()
遍历for (int i=0; i<da->size; i++)for x in &v(Iterator)
排序手写或 qsortv.sort()
释放da_free(da)自动 Drop

6.3 一个完整的功能:C vs Rust


// 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 —— 同样的功能

#[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 —— 忘了一个检查就是 bug

int* p = realloc(arr, new_size * sizeof(int));

if (p == NULL) { /* 必须检查 */ } // 如果忘了——灾难


// 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——uthash 的宏

HASH_FIND_INT(users, &id, found);

// 宏:不检查 users 的类型、id 的类型、found 的类型


// Rust——方法是类型安全的

let found: Option<&User> = users.get(&id);

// 如果 id 的类型和 key 类型不匹配——编译错误

"Iterator 不就是函数指针回调吗?"

C 程序员:"Rust 的 for x in &v 让我想起 C 中函数指针的回调遍历,只是语法不一样。"

Rust:"不只是语法。迭代器是惰性的——你可以构建一个操作链(.filter().map()),只有最后调用 .collect() 时才真正执行。C 的函数回调做不到这种惰性组合——每个回调都是立即执行的。而且迭代器适配器链在编译期会被优化成手写循环——零运行时开销。"


// C —— 立即执行,不能链式组合

void process_if_even(void* ctx, int val) {

if (val % 2 == 0) {

process(val * 2);

}

}

foreach(arr, n, process_if_even); // 现在就执行了


// Rust——惰性组合

let result: Vec<_> = arr.iter()

.filter(|x| *x % 2 == 0) // 只是声明了"要过滤"

.map(|x| x * 2) // 只是声明了"要变换"

.collect(); // 到这里才真正执行


八、小结

8.1 C 的问题


C 没有标准容器库

→ 动态数组、哈希表、集合都需要手写

→ 每种容器的遍历方式不同

→ 没有统一的访问语法([] 只对原生数组有效)

→ 内存管理全部手动

8.2 Rust 的解决

问题Rust 的解决
没有标准动态数组Vec<T>(类型安全、自动扩容、自动释放)
没有标准哈希表HashMap<K, V>(标准库提供)
没有统一访问语法Index trait——任何容器都支持 container[i]
没有统一遍历方式Iterator trait——所有容器都用 for x in &container
内存管理手动容器类型实现 Drop——离开作用域自动释放

8.3 容器 + Trait 的关系


容器(Vec、HashMap、String……)

↓ 实现了

Index trait → container[i] 语法

IntoIterator traitfor x in container 语法

↓

调用者不需要知道容器内部是什么

只需要知道:它实现了 Index + IntoIterator

一句话总结:C 没有标准容器库——动态数组手写、哈希表第三方、遍历方式各不相同。Rust 的标准容器(VecHashMap 等)通过 Index trait(统一的 [] 访问)和 Iterator trait(统一的 for 遍历)与语言语法深度集成——让"操作容器"和"操作原生数组"一样自然,同时保持类型安全和内存安全。