📚 Rust 课程系列

  1. 课程概览
  2. 基础语法(一):变量、数据类型与字符串
  3. 切片(Slice):序列的借用视图
  4. 基础语法(二):运算符、表达式与控制流
  5. 函数与输入输出
  6. 所有权、借用与生命周期
  7. 结构体
  8. 枚举
  9. 模式匹配
  10. 类型系统:泛型、trait 与多态
  11. 集合与容器
  12. 错误处理与 Panic 恢复
  13. 模块、属性与宏
  14. 智能指针、迭代器与闭包(本文)
  15. 并发与异步编程
  16. Unsafe Rust 与常用 trait 详解
  17. 工具链、Cargo 与外部 crate
  18. 最佳实践、性能与调试

智能指针提供超越引用的所有权语义,迭代器是 Rust 零成本抽象的典范,闭包则是配合迭代器的匿名函数。本章把三者放在一起:Box/Rc/Arc/Cell/RefCell 解决所有权与内部可变性,迭代器与闭包支撑函数式的数据流水线。深入内容可参考文末延伸阅读。

智能指针

Rust 的"智能指针"不仅是指针,还携带所有权语义与运行时行为。核心思路:编译时借用规则(&/&mut)优先;无法满足需求时才引入智能指针

💡 提示:Rust 的智能指针与 C++ 设计哲学不同——C++ 依赖运行时行为(如 shared_ptr 的引用计数),Rust 优先在编译期保证安全,只在必要时才退到运行时检查(如 RefCell)。

🔄 对比Box<T> ≈ C++ unique_ptr<T>(独占所有权),Rc<T> ≈ C++ shared_ptr<T>(共享所有权),但 Rust 编译器在编译期强制执行所有权规则,而非依赖程序员自律。

速查表

智能指针所有权可变性线程安全典型用途
Box<T>独占外部可变✅ (若 T: Send+Sync)递归数据结构、堆上大对象
Rc<T>共享(引用计数)需配合 RefCell单线程多所有者(树/图)
Arc<T>共享(原子引用计数)需配合 Mutex/RwLock多线程共享只读数据
Cell<T>独占内部可变(按值)小型 Copy 类型的标志位/缓存
RefCell<T>独占内部可变(运行时借用检查)单线程需绕开编译时借用规则
Weak<T>不持有所有权取决于 Rc/Arc打破循环引用

Box<T>

将值放到堆上,最简单的智能指针。核心用途:递归数据结构(编译时大小未知)和堆上大对象。

1
2
let b = Box::new(5);
let boxed_array: Box<[i32]> = Box::new([1, 2, 3]);

💡 提示Box 满足编译器对"大小已知"的要求——递归类型(如 enum List { Cons(i32, Box<List>), Nil })必须用 Box 包裹递归分支,否则编译器无法确定大小。

⚠️ 注意Box 是零开销抽象——解引用 *b 与栈上访问性能一致,唯一额外开销是一次堆分配。

Rc<T> 与 Arc<T>

引用计数智能指针,允许多个所有者共享同一数据。Rc 用于单线程,Arc 用于多线程(原子计数,开销略高)。

1
2
3
4
5
use std::rc::Rc;

let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a); // 增加引用计数,不复制数据
println!("count = {}", Rc::strong_count(&a)); // 2
1
2
3
4
use std::sync::Arc;
// Arc: 多线程共享——clone 是原子操作,略慢于 Rc
let data = Arc::new(vec![1, 2, 3]);
let d = Arc::clone(&data); // 传给线程即可,Arc 保证线程安全

💡 提示Rc::clone 只增加计数(O(1)),不深拷贝数据。Arc 的 clone 使用原子操作,比 Rc 慢约 2-3 倍,但仍是 O(1)。

⚠️ 注意Rc<T> 不是 Send/Sync——跨线程传递会在编译期报错。多线程场景必须用 Arc<T>

⚠️ 注意:循环引用会导致引用计数永远无法归零,造成内存泄漏。需要父指针时用 Weak<T> 断开循环:

1
2
3
4
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node { parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>> }

🔄 对比:C++ 用 weak_ptr 打破 shared_ptr 的循环引用,Rust 用 Weak<T> 打破 Rc 的循环引用——机制相同。

Cell<T> 与 RefCell<T>(内部可变性)

内部可变性(Interior Mutability)是 Rust 的设计模式:在外部 &T(不可变引用)下修改内部数据。这是对编译时借用规则的有条件绕开。

💡 提示:为什么需要内部可变性?有些场景下,从调用者角度看数据"逻辑上不可变",但内部实现需修改(如缓存、引用计数、惰性初始化)。编译时借用规则无法区分"逻辑不可变"和"物理不可变",故需运行时机制。

  • Cell<T>:按值存取,适合 Copy 类型。通过 get/set/replace 操作,不提供引用借用。
1
2
3
4
use std::cell::Cell;
let c = Cell::new(5);
c.set(10);
let v = c.get(); // Copy 出来
  • RefCell<T>:运行时借用检查。borrow() 返回 Ref<T>borrow_mut() 返回 RefMut<T>。违反借用规则时 panic
1
2
3
4
5
6
7
8
9
use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node { value: i32, children: Vec<Rc<RefCell<Node>>> }

let leaf = Rc::new(RefCell::new(Node { value: 3, children: vec![] }));
let branch = Rc::new(RefCell::new(Node { value: 5, children: vec![Rc::clone(&leaf)] }));
leaf.borrow_mut().value = 10; // 通过 RefCell 修改,即使 leaf 被 Rc 共享

⚠️ 注意RefCellborrow_mut() 与已有 borrow() 同时存在时会 panic,而非编译错误。这是"将错误从编译期推迟到运行期"的权衡——务必在逻辑上保证不同时持有可变与不可变借用。

💡 提示Rc<RefCell<T>> 是单线程中"多所有者 + 可变访问"的经典组合。多线程对应物是 Arc<Mutex<T>>(详见第 13 章)。

Cow 与 Pin(进阶)

🔬 进阶:以下内容在特定场景下有用,初学者可跳过。

Cow<B>(写时克隆):借用或拥有的抽象——数据无需修改时零开销借用,需修改时才克隆。

1
2
3
4
5
6
7
8
9
use std::borrow::Cow;

fn remove_spaces<'a>(input: &'a str) -> Cow<'a, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "")) // 需要修改 → 拥有副本
} else {
Cow::Borrowed(input) // 无需修改 → 零开销借用
}
}

🔄 对比Cow ≈ C++ 的 copy-on-write 惯用法,但 Rust 在类型层面区分了借用与拥有两种状态。

Pin<P> 与 Unpin:确保值在内存中不被移动,主要用于 async 生成的自引用 Future。大多数类型自动实现 Unpin(可安全移动),只有自引用类型需要 Pin 约束。

1
2
3
use std::pin::Pin;
let mut val = 5;
let pinned = Pin::new(&mut val); // 普通类型无需 Pin,这里仅演示 API

⚠️ 注意:日常编码几乎不需要手动使用 Pin——async 运行时(Tokio 等)会自动处理。只有实现自引用类型时才需深入理解。

智能指针选择指南

  1. 优先编译时借用规则&/&mut)——零开销,编译期保证安全
  2. 需要堆分配或递归类型 → Box<T>
  3. 单线程多所有者 → Rc<T>;多线程 → Arc<T>
  4. 需要内部可变性:Copy 类型 → Cell<T>,非 CopyRefCell<T>
  5. 多线程可变共享 → Arc<Mutex<T>>Arc<RwLock<T>>
  6. 循环引用 → Weak<T> 打破

💡 提示:更详细的对比、与 C++ 智能指针的对照、混合使用指南,见 Rust 智能指针

迭代器

Rust 的 for 循环本质是迭代器语法糖——适配器惰性求值,由消费者驱动计算。

基本概念:Iterator trait

任何实现 Iterator trait 的类型都可遍历,只需定义 Item 类型和 next() 方法:

1
2
3
4
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}

next() 有值返回 Some,迭代结束返回 None

1
2
3
4
5
6
let v = vec![1, 2, 3];
let mut iter = v.iter(); // 产生 &i32
assert_eq!(iter.next(), Some(&1));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), None);

三种创建方式

与所有权模型一一对应:

方法产生元素所有权语义
iter()&T借用(集合仍可用)
iter_mut()&mut T可变借用(可修改元素)
into_iter()T获取所有权(消费集合)
1
2
3
let mut v = vec![1, 2, 3];
for x in v.iter_mut() { *x *= 2; } // v 变为 [2, 4, 6],仍可用
let sum: i32 = v.into_iter().sum(); // 12,v 被消费,此后不可用

💡 提示for x in &v 等价于 v.iter()for x in &mut v 等价于 v.iter_mut()for x in v 等价于 v.into_iter()——for 循环会隐式调用 IntoIterator

惰性适配器

适配器返回新迭代器,本身不计算(惰性):

适配器作用示例
map转换每个元素(1 进 1 出).map(|x| x * 2)
filter保留满足条件的元素.filter(|&x| x > 0)
filter_map过滤并转换(闭包返回 Option.filter_map(|s| s.parse().ok())
flat_map映射后扁平化(1 进多出).flat_map(|s| s.split_whitespace())
enumerate添加索引.enumerate() -> (usize, T)
zip按索引配对两个迭代器.zip(other)
take / skip取 / 跳过前 n 个.take(5)
take_while / skip_while按条件取 / 跳过连续元素.take_while(|&x| x < 5)
chain连接两个迭代器.chain(other)
rev反转(需 DoubleEndedIterator.rev()
cloned / copied&T -> T.copied()
step_by按步长取元素.step_by(2)
scan带可变状态的转换.scan(init, |st, x| ...)
cycle无限循环(需 Clone.cycle().take(n)

💡 提示filter 的闭包接收的是元素的引用&Self::Item),故常用 |&x| 模式解构引用——这是 |x| *x 的语法糖,让闭包体内直接用值而非引用。

消费者

消费者急切地触发实际计算:

消费者作用返回
collect收集为集合(依赖 FromIteratorVec/HashMap/HashSet/String
sum / product求和 / 求积数值
count计数usize
max / min最大 / 最小值Option<T>
any / all任一 / 全部满足bool(短路求值)
find第一个满足条件的元素Option<T>
position第一个满足条件的索引Option<usize>
fold累积计算(可改变类型)B
reduce同类型归约Option<T>
1
2
3
4
5
let v = vec![1, 2, 3, 4, 5];
let sum: i32 = v.iter().sum(); // 15
let has_large = v.iter().any(|&x| x > 3); // true(短路:找到即停)
let max = v.iter().max(); // Some(&5)
let pos = v.iter().position(|&x| x > 3); // Some(3)

collect:收集为不同类型

collect 依赖 FromIterator,需显式标注目标类型:

1
2
3
4
5
6
use std::collections::{HashMap, HashSet};
let v = vec![1, 2, 2, 3, 3, 4];
let set: HashSet<i32> = v.iter().copied().collect(); // {1,2,3,4}(自动去重)
let map: HashMap<i32, String> = v.iter()
.map(|&x| (x, format!("n{}", x))).collect(); // 键值对 -> HashMap
let s: String = vec!['h', 'i'].into_iter().collect(); // "hi"(String: FromIterator<char>)

fold 与 reduce

累积计算的核心——fold 提供初始值、可改变累积类型;reduce 用首元素作初始值、类型不变:

1
2
3
4
5
6
let v = vec![1, 2, 3, 4, 5];
// fold:显式初始值,可改变累积类型(这里连接成 String)
let sentence: String = vec!["Hello", " ", "World"]
.iter().fold(String::new(), |mut acc, w| { acc.push_str(w); acc });
// reduce:用首元素作初始值,类型不变,空集合返回 None
let sum = v.iter().copied().reduce(|a, b| a + b); // Some(15)

💡 提示fold 对空迭代器安全(直接返回初始值);reduce 空集合返回 None

filter_map:过滤并转换

一次遍历完成"过滤 + 转换",比 filter().map() 更简洁高效——闭包返回 Some(value) 保留并转换,None 丢弃:

1
2
let nums: Vec<i32> = vec!["1", "2", "abc", "3"]
.iter().filter_map(|s| s.parse().ok()).collect(); // [1, 2, 3],自动丢弃 None

迭代器链与惰性求值

多个适配器组合,体现函数式数据流水线:

1
2
3
4
5
6
let result: Vec<i32> = (1..=10)
.filter(|x| x % 2 == 0) // [2, 4, 6, 8, 10]
.map(|x| x * x) // [4, 16, 36, 64, 100]
.filter(|x| x > &20) // [36, 64, 100]
.take(2) // [36, 64]
.collect();

💡 提示:迭代器链是零成本抽象:编译后等价于手写循环,常因消除边界检查而比手写循环更快。

⚠️ 注意:忘记消费迭代器是常见错误——写了 .map(...) 却没有 .collect()for,代码编译通过但什么都不做。编译器会发出"unused must_use"警告,务必留意。

自定义迭代器

实现 Iterator trait 的 Itemnext() 即可,自动获得 map/filter/sum 等所有方法。

1
2
3
4
5
6
7
8
9
struct Counter { count: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 { self.count += 1; Some(self.count) } else { None }
}
}
// Counter { count: 0 } 现在可用 for 遍历,也可链式调用适配器:
let doubled: Vec<u32> = Counter { count: 0 }.map(|x| x * 2).collect(); // [2, 4, 6, 8, 10]

💡 提示:切片(非迭代器)还有 chunks(n)(不重叠分块,末块可能不足)与 windows(n)(滑动窗口,相邻窗口重叠)方法,常用于分批处理与移动平均(详见《切片(Slice)》)。itertools crate 提供 fold_while(条件累积提前终止)、multiunzip 等更多扩展。

闭包

闭包是可捕获环境变量的匿名函数,是配合迭代器的函数式利器,语法 |param| body

闭包基础

1
2
3
let factor = 2;
let multiply = |x| x * factor; // 捕获 factor 的引用
assert_eq!(multiply(5), 10);

闭包与迭代器搭配

链式数据处理的惯用写法——迭代器适配器接收闭包作为参数:

1
2
3
4
let v: Vec<i32> = vec![1, 2, 3]
.into_iter()
.map(|x| x * x)
.collect();

捕获 trait:Fn / FnMut / FnOnce

闭包根据捕获方式自动实现以下 trait,决定可被调用的方式:

Trait捕获方式可调用次数典型场景
Fn不可变借用 &T多次只读访问环境
FnMut可变借用 &mut T多次修改环境变量
FnOnce获取所有权 T一次消费环境变量(如 move 闭包传入线程)
1
2
3
4
5
6
7
8
fn apply<F>(f: F) -> i32
where F: Fn(i32) -> i32 {
f(5) // Fn 可多次调用
}

let x = 10;
let add_x = |y| y + x; // 实现 Fn(只借用 x)
println!("{}", apply(add_x)); // 15

💡 提示:Rust 编译器按"最小权限"原则自动推断闭包 trait——只需 &T 就不会 &mut T,只需 &mut T 就不会 move,避免不必要的限制。

🔄 对比:Rust 闭包的 Fn/FnMut/FnOnce 三级 trait 比 C++ 按值/引用捕获更精细——编译器自动选择最小权限,而非依赖程序员手动指定捕获列表。Go 闭包则始终按引用捕获,可能导致意外的数据竞争。

move 闭包

当闭包需跨越作用域(如传入 thread::spawn)时,环境变量必须 move 而非借用——用 move 关键字强制移动捕获:

1
2
let factor = 2;
let c = move |x| x * factor; // factor 被移动进闭包,闭包拥有其所有权

⚠️ 注意:跨线程传递闭包几乎都需要 move,否则借用无法跨越线程生命周期,编译器会报"lifetime may not live long enough"。move 只影响捕获方式,闭包最终实现 Fn/FnMut/FnOnce 仍取决于它如何使用捕获变量。


延伸阅读:

  • Rust 智能指针Box/Rc/Arc/RefCell/Cell 的对比、与 C++ 智能指针的对照、混合使用与选择指南)。