第 11 章:你以为换个语言,集合类就换个名字这么简单?

Naive!

Java 的 ArrayListStringHashMap 在 GC 的保护下随便用。Rust 的 VecStringHashMap 每一次操作都要过所有权系统的审查。

这不是负担,这是进化。

Java 集合:GC 庇护下的放纵

Java 的"舒适圈"

// Java 的任性代码
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");

String first = list.get(0);  // 随便取
String copy = new String(first);  // 随便复制
list.clear();  // 随便清空

// GC 负责一切你什么都不用担心

爽吗?当然爽。高效吗?呵呵。

隐藏的成本

Java 集合的每一次操作都有隐性成本:

  • 装箱拆箱 - Integer vs int 的开销
  • GC 压力 - 频繁分配回收的暂停
  • 内存碎片 - 对象散布在堆上各处
  • 缓存不友好 - 引用跳跃,缓存命中率低

舒适是有代价的。

Vec:ArrayList 的完美进化

创建和基本操作

// 创建 Vec
let mut vec = Vec::new();  // 空向量
let mut vec = vec![1, 2, 3];  // 宏创建

// 添加元素
vec.push(4);
vec.push(5);

// 访问元素
let first = &vec[0];  // 借用,不转移所有权
let first = vec.get(0);  // 返回 Option<&T>,更安全

// 遍历
for item in &vec {  // 借用遍历
    println!("{}", item);
}

for item in vec {  // 消费遍历,vec 失效
    println!("{}", item);
}

看到区别了吗?每个操作都要考虑所有权。

所有权的深层影响

fn process_vec() {
    let mut vec = vec![1, 2, 3];

    let first = vec[0];  // Copy trait,直接复制
    println!("First: {}", first);

    vec.push(4);  // vec 依然有效,因为 i32 实现了 Copy

    // 如果是 String 呢?
    let mut string_vec = vec!["hello".to_string(), "world".to_string()];

    // let first_string = string_vec[0];  // 编译错误!String 没有 Copy
    let first_string = &string_vec[0];  // 只能借用

    println!("First string: {}", first_string);
}

这种限制看似麻烦,实际上保证了内存安全。

性能优势:连续内存布局

// Rust Vec:连续内存,缓存友好
let vec: Vec<i32> = (0..1000000).collect();

// Java ArrayList:对象引用数组,缓存不友好
// List<Integer> list = IntStream.range(0, 1000000)
//     .boxed().collect(Collectors.toList());

Vec 的元素连续存储,ArrayList 存储的是引用。 谁的性能更好?显而易见。

String:不再是"不可变"的字符串

Java String:不可变的假象

String s = "hello";
s += " world";  // 创建新对象,原对象变垃圾
// 每次"修改"都创建新对象GC 压力巨大

Rust String:真正的可变字符串

let mut s = String::from("hello");
s.push_str(" world");  // 原地修改,无额外分配

// 高效的字符串构建
let s = format!("{} {}", "hello", "world");  // 一次分配

// 字符串拼接
let mut result = String::new();
result.push_str("hello");
result.push(' ');
result.push_str("world");

真正的可变性,真正的性能。

String vs &str:所有权的经典案例

fn string_processing() {
    let owned = String::from("hello world");  // 拥有数据
    let borrowed = "hello world";  // 借用静态数据

    // 函数签名的选择
    process_owned(owned);  // 转移所有权
    // process_owned(owned);  // 编译错误!owned 已失效

    process_borrowed(borrowed);  // 借用
    process_borrowed(borrowed);  // 可以重复借用
}

fn process_owned(s: String) {  // 获得所有权
    println!("{}", s);
}

fn process_borrowed(s: &str) {  // 只是借用
    println!("{}", s);
}

API 设计的哲学:能借用就不要夺取所有权。

HashMap<K,V>:哈希表的所有权挑战

基本操作

use std::collections::HashMap;

let mut map = HashMap::new();

// 插入键值对
map.insert("key1".to_string(), "value1".to_string());
map.insert("key2".to_string(), "value2".to_string());

// 访问值
let value = map.get("key1");  // 返回 Option<&V>
match value {
    Some(v) => println!("Found: {}", v),
    None => println!("Not found"),
}

// 遍历
for (key, value) in &map {  // 借用遍历
    println!("{}: {}", key, value);
}

所有权的复杂性

fn hashmap_ownership() {
    let mut map = HashMap::new();

    let key = String::from("important");
    let value = String::from("data");

    map.insert(key, value);  // key 和 value 的所有权被转移

    // println!("{}", key);  // 编译错误!key 已被移动

    // 解决方案1:克隆
    let key = String::from("important");
    let value = String::from("data");
    map.insert(key.clone(), value.clone());  // 克隆后插入
    println!("{}", key);  // key 依然有效

    // 解决方案2:使用引用作为键(生命周期复杂)
    // 解决方案3:使用 &str 作为键(适合静态字符串)
}

HashMap 的每次插入都可能转移所有权。 这强制你思考数据的生命周期。

高级模式:entry API

// Java 的丑陋模式
// if (!map.containsKey(key)) {
//     map.put(key, new ArrayList<>());
// }
// map.get(key).add(value);

// Rust 的优雅模式
map.entry(key)
   .or_insert_with(Vec::new)
   .push(value);

// 或者更简洁
map.entry(key).or_default().push(value);

entry API 避免了重复查找,性能更好。

内存布局:连续 vs 分散

Java 集合的内存布局

ArrayList<String>:
┌─────┐    ┌────────┐
│ ref ├───►│ array  │
└─────┘    │ [ref1] ├─┐
           │ [ref2] ├─┼─┐
           │ [ref3] ├─┼─┼─┐
           └────────┘ │ │ │
                      │ │ └─► "world"
                      │ └───► "hello"
                      └─────► "foo"

引用跳跃,缓存不友好。

Rust 集合的内存布局

Vec<String>:
┌─────┐    ┌─────────────────┐
│Vec  ├───►│ String1         │
└─────┘    │ String2         │
           │ String3         │
           └─────────────────┘

连续存储,缓存友好,性能更佳。

实战技巧:避免不必要的克隆

反模式:过度克隆

// 糟糕:不必要的克隆
fn bad_example(vec: Vec<String>) -> Vec<String> {
    let mut result = Vec::new();
    for item in vec {
        result.push(item.clone());  // 不必要的克隆
    }
    result
}

良好模式:智能借用

// 优秀:避免不必要的所有权转移
fn good_example(vec: &[String]) -> Vec<&str> {
    vec.iter().map(|s| s.as_str()).collect()
}

// 或者使用迭代器
fn iterator_example(vec: Vec<String>) -> Vec<String> {
    vec.into_iter()  // 消费迭代器,转移所有权
       .filter(|s| !s.is_empty())
       .collect()
}

理解何时需要所有权,何时只需要借用。

写在最后:集合的哲学转变

Java 集合的哲学:给你便利,GC 负责后果。

Rust 集合的哲学:给你控制,你负责思考。

这种转变带来的收益:

  • 内存效率 - 无 GC 开销,无装箱成本
  • 性能可预测 - 无突然的 GC 暂停
  • 缓存友好 - 连续内存布局
  • 线程安全 - 所有权保证无数据竞争

当你习惯了 Rust 的集合操作,你会发现:

  • 代码更安全了(编译期保证)
  • 性能更可控了(无 GC 惊喜)
  • 内存使用更高效了(精确控制)
  • 并发编程更简单了(所有权保证安全)

这就是现代系统编程语言的集合应该有的样子。

下一章我们要学习 Rust 的错误处理进阶技巧。准备好看看如何构建真正健壮的应用程序了吗?