闭包与 Cow
闭包如何捕获环境、
Fn/FnMut/FnOnce的区别与Cow。
闭包(Closure)
语法与类型推断
闭包是能捕获环境的匿名函数。|参数| 表达式:
rust
fn main() {
// 完整标注
let add = |a: i32, b: i32| -> i32 { a + b };
// 类型可推断
let double = |x| x * 2;
// 多语句要花括号
let describe = |n: i32| {
if n > 0 {
format!("正数 {n}")
} else {
format!("非正数 {n}")
}
};
println!("{} {} {}", add(1, 2), double(21), describe(-5));
// 泛型函数一次调用后闭包类型就固定了
let id = |x| x;
let s = id(String::from("first"));
// let n = id(1); // 编译错误:id 已经被推断为 Fn(String) -> String
println!("{s}");
}输出:
3 42 非正数 -5
闭包类型是唯一的匿名类型:每个闭包字面量都有自己的类型,即使签名相同也不能互相赋值。 所以「闭包类型」只能通过 impl Fn、&dyn Fn 或泛型参数来表达。
💡 对照:Python 的
lambda同样能捕获环境,但捕获的是变量名(后绑定,容易踩坑); JS 的箭头函数捕获的是词法作用域的绑定;Rust 捕获的是变量的所有权或引用—— 而且捕获方式由「闭包体怎么用这个变量」自动推导。
三种捕获方式:FnOnce / FnMut / Fn
闭包对环境的借用强度由内部怎么用决定:
| trait | 调用时接收 | 捕获方式 | 能调用几次 | 例子 |
|---|---|---|---|---|
FnOnce | self(按值) | 移动或消费捕获的变量 | 1 次 | move || s(把 s 交出去) |
FnMut | &mut self | 可变借用 | 多次,可改状态 | || count += 1 |
Fn | &self | 不可变借用 | 任意次,不改状态 | |x| x + 1 |
包含关系是单向的:
Fn ⊂ FnMut ⊂ FnOnce
(能当 Fn 用的,一定能当 FnMut 用;能当 FnMut 用的,一定能当 FnOnce 用)
反过来不成立:FnOnce 的闭包不能要求它实现 Fn 需要 FnOnce 的地方
├──────────────────────────────┤
需要 FnMut 的地方
├───────────────────┤
需要 Fn 的地方
├─────────┤rust
fn call_once<F: FnOnce() -> String>(f: F) -> String {
f()
}
fn call_mut<F: FnMut() -> u32>(mut f: F) -> u32 {
f() + f() // 连续调用两次,所以至少需要 FnMut
}
fn call_many<F: Fn() -> u32>(f: F) -> u32 {
f() + f() + f()
}
fn main() {
let owned = String::from("moved out");
let once = move || owned; // 消费 captured 变量 -> FnOnce
println!("{}", call_once(once));
let mut n = 0;
let mut counter = || {
n += 1; // 可变借用 -> FnMut(也满足 FnOnce)
n
};
println!("counter = {}", call_mut(&mut counter));
// FnMut 不能传给要 Fn 的函数:
// println!("{}", call_many(&mut counter)); // E0525
let pure = || 7; // 不碰环境 -> Fn(也满足 FnMut、FnOnce)
println!("{}", call_many(pure));
}输出:
moved out counter = 3 21
🧠 原理:编译器先看闭包体怎么用捕获变量:只读 → 实现
Fn;要改 →FnMut; 要移动/消费 →FnOnce。然后同时实现所有更弱的 trait。这就是 「Fn: FnMut: FnOnce」这行代码的来源——它表示Fn的 supertrait 是FnMut。
⚠️ 陷阱:把
FnMut闭包传给需要Fn的函数(比如HashMap::entry、Option::unwrap_or_else的某些用法、或者同时把闭包存两份)会报E0525:expected a closure that implements the Fn trait, but this closure only implements FnMut。 修法是用RefCell/Cell把可变状态包起来,让闭包本身只需&self。
move 关键字
move 强制闭包按值捕获(拿所有权),而不是按引用:
rust
fn main() {
let name = String::from("Rust");
let by_ref = || println!("借用: {name}"); // 捕获 &name
by_ref();
println!("外部仍然可用: {name}"); // ✅ 只借用了
let by_move = move || println!("移入: {name}"); // 捕获 name 本身
by_move();
// println!("{name}"); // ❌ E0382:name 已经被移进闭包
}输出:
借用: Rust 外部仍然可用: Rust 移入: Rust
move 与 Copy 类型:如果捕获的变量是 Copy(i32、bool、&T、u64 等), move 做的是复制而不是移动,原变量照旧可用:
rust
fn main() {
let n = 10; // i32: Copy
let f = move || n + 1; // 复制了一份 n 进闭包
println!("{}", f());
println!("外部 n 还在: {n}"); // ✅ Copy 类型不受影响
let s = &n; // 引用本身也是 Copy
let g = move || *s + 2; // 复制的是「引用」,不是 n
println!("{} {}", g(), s);
}输出:
11 外部 n 还在: 10 12 10
⚠️ 陷阱:
move复制的是引用时很危险——move闭包可能比被引用的数据活得更久。 典型场景是thread::spawn(move || ...)里捕获了&local:编译器会报E0597/borrowed value does not live long enough(要'static)。这时必须在闭包外 先clone出所有权,或者用Arc共享。
| 场景 | 用不用 move |
|---|---|
| 闭包在同一个作用域内立即调用 | 不用,借用更省 |
闭包被返回 / 存进结构体 / 交给 thread::spawn | 必须 move |
捕获的是 Copy 小类型 | 用 move 无害(等于复制) |
捕获的是引用 &T,但闭包会活得久 | 危险,改成捕获 T 或 Arc<T> |
闭包作为参数与返回值
三种接收方式,能力和开销不同:
rust
// 1) 泛型 + Fn:静态分发,零开销,每个具体闭包类型生成一份代码
fn apply_generic<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
// 2) impl Fn:泛型的语法糖,同样静态分发
fn apply_impl(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
// 3) &dyn Fn:动态分发,一次虚表跳转,但不会让代码膨胀,可以放进集合
fn apply_dyn(f: &dyn Fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn main() {
let inc = |x: i32| x + 1;
println!("{} {} {}", apply_generic(inc, 1), apply_impl(inc, 2), apply_dyn(&inc, 3));
}输出:
2 3 4
需要「根据条件返回不同闭包」或「返回捕获了局部变量的闭包」时:
rust
// 返回捕获局部变量的闭包:必须 move
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n // move:n 被移进闭包,否则 n 会在函数返回时析构
}
// 返回有状态的闭包:用 FnMut
fn make_counter() -> impl FnMut() -> u32 {
let mut count = 0;
move || {
count += 1;
count
}
}
// 需要在运行期选不同闭包(签名相同但类型不同):只有 Box<dyn Fn> 能做到
fn pick(kind: &str) -> Box<dyn Fn(i32) -> i32> {
match kind {
"double" => Box::new(|x| x * 2),
_ => Box::new(|x| x + 1),
}
}
fn main() {
let add5 = make_adder(5);
println!("{} {}", add5(1), add5(2)); // 闭包可重复调用
let mut counter = make_counter();
println!("{} {} {}", counter(), counter(), counter());
println!("{} {}", pick("double")(10), pick("other")(10));
}输出:
6 7 1 2 3 20 11
⚠️ 陷阱:返回
impl Fn时如果忘了move,会报E0373 closure may outlive the current function, but it borrows ...: 局部变量在函数返回时就被 drop,闭包不能留着它的引用。 加move就对了——闭包把变量搬进自己的环境,活多久都行。
⚠️ 陷阱:
fn f() -> impl Fn() -> usize这种签名如果要借用参数,必须显式标生命周期:fn f<'a>(s: &'a str) -> impl Fn() -> usize + 'a。Rust 2024 中返回位置的impl Trait会捕获所有在作用域内的生命周期(不同于 2021 的隐式'static倾向), 但显式标注仍然更清晰。
闭包 vs 函数指针
| 对比项 | 闭包 |x| x + 1 | 函数 fn f(x: i32) -> i32 / 函数指针 fn(i32) -> i32 |
|---|---|---|
| 能否捕获环境 | ✅ 能 | ❌ 不能(无环境) |
| 类型 | 唯一的匿名类型 | 具体函数项类型 → 可强制转换成 fn 指针 |
实现 Fn/FnMut/FnOnce | ✅ | ✅(三类都实现) |
| 大小 | 捕获了多少就多大(可能 0 字节) | 8 字节指针 |
能否放进 static / FFI | ❌(除非无捕获且强转) | ✅ |
| 泛型参数的写法 | 必须 impl Fn(..) 或 F: Fn(..) | 可直接写 fn(i32) -> i32 |
rust
fn double(x: i32) -> i32 {
x * 2
}
fn apply_fn(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn apply_closure<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
fn main() {
// 无捕获闭包可以强转成函数指针
let c = |x: i32| x + 3;
println!("fn 参数收闭包 = {}", apply_fn(c, 1));
println!("fn 参数收函数 = {}", apply_fn(double, 1));
// 有捕获的闭包不能当 fn 指针:let p: fn(i32) -> i32 = c; 会报 E0308
let offset = 100;
let capturing = |x: i32| x + offset;
println!("泛型参数两边都能收 = {}", apply_closure(capturing, 1));
println!("函数也能传泛型参数 = {}", apply_closure(double, 1));
}输出:
fn 参数收闭包 = 4 fn 参数收函数 = 2 泛型参数两边都能收 = 101 函数也能传泛型参数 = 2
选用建议:对外 API 想同时接受函数和闭包 → 用 impl Fn; 要把闭包存进 C 结构体或传 FFI → 用 fn 指针; 要在运行期二选一 → 用 Box<dyn Fn>。
迭代器里的 Fn / FnMut:为什么 iter_mut 要 FnMut
迭代器适配器的方法签名决定了闭包需要哪个 trait:
| 方法 | 闭包约束 | 说明 |
|---|---|---|
map | FnMut(Self::Item) -> B | 每个元素调用一次,可维护内部状态 |
filter | FnMut(&Self::Item) -> bool | 同上,但要返回 bool |
fold | FnMut(B, Self::Item) -> B | 累加器就是「闭包内部状态」 |
for_each | FnMut(Self::Item) | 就地消费 |
sort_by | FnMut(&T, &T) -> Ordering | 比较器 |
unwrap_or_else | FnOnce() -> T | 最多调用一次,可以消费环境 |
rust
fn main() {
let data = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<i32> = data.iter().copied().filter(|n| n % 2 == 0).collect();
let doubled: Vec<i32> = evens.iter().map(|n| n * 2).collect();
let sum: i32 = doubled.iter().fold(0, |acc, n| acc + n);
println!("evens = {evens:?}");
println!("doubled = {doubled:?}");
println!("sum = {sum}");
// iter_mut:闭包必须能改外部变量,所以约束是 FnMut
let mut nums = vec![1, 2, 3];
let mut changed = 0;
for n in nums.iter_mut() {
*n += 1;
changed += 1; // 闭包(for 循环体)捕获了 &mut changed
}
println!("nums = {nums:?}, changed = {changed}");
}输出:
evens = [2, 4, 6] doubled = [4, 8, 12] sum = 24 nums = [2, 3, 4], changed = 3
map 的闭包在 FnMut 里也能持有状态——这解释了为什么可以有「累加和」这种写法:
rust
fn main() {
let values = [1, 2, 3, 4];
// 闭包捕获 &mut acc,多次调用 -> 必须是 FnMut
let mut acc = 0;
let running: Vec<i32> = values
.iter()
.map(|v| {
acc += v; // 修改捕获变量,所以这个闭包是 FnMut
acc
})
.collect();
println!("running = {running:?}");
}输出:
running = [1, 3, 6, 10]
🧠 原理:
map之所以约束FnMut而不是Fn,正是为了允许这种「带状态的映射」。 如果约束成Fn,acc += v这种闭包就传不进去了(因为Fn的调用只有&self, 不能改捕获的变量)。for_each也是FnMut,同理。
闭包捕获导致的借用冲突:经典案例与修法
案例:遍历集合的同时修改集合本身——闭包已经借走了集合。
rust
fn main() {
let mut nums = vec![1, 2, 3];
let threshold = 2;
nums.retain(|n| {
nums.push(*n); // ❌ 闭包已可变借用 nums,这里第二次可变借用
*n != threshold
});
println!("{nums:?}");
}error[E0499]: cannot borrow `nums` as mutable more than once at a time
--> e0502_closure.rs:5:5
|
5 | nums.retain(|n| {
| ^ ------ --- first mutable borrow occurs here
| | |
| _____| first borrow later used by call
| | |
6 | | nums.push(*n); // 编译错误:闭包已借用 nums
| | ---- first borrow occurs due to use of `nums` in closure
7 | | *n != threshold
8 | | });
| |______^ second mutable borrow occurs here修法一:先收集决策,再统一修改(最常用)
rust
fn main() {
let mut nums = vec![1, 2, 3];
let threshold = 2;
// 只读借用期间做决策,决策结果是一个新 Vec
let kept: Vec<i32> = nums.iter().copied().filter(|n| *n != threshold).collect();
nums = kept; // 借用结束后再赋值
println!("{nums:?}");
}输出:
[1, 3]
修法二:用 mem::take 把集合「换出来」——记住 「std::mem 常用函数」一节的 std::mem::take:
rust
fn main() {
let mut nums = vec![1, 2, 3];
let threshold = 2;
let mut taken = std::mem::take(&mut nums); // nums 变成空 Vec,taken 拿到所有权
taken.retain(|n| *n != threshold); // 现在可以自由改 taken
nums = taken; // 放回去
println!("{nums:?}");
}输出:
[1, 3]
修法三:改用索引 + swap_remove / drain(避免闭包与环境别名)
rust
fn main() {
let mut nums = vec![1, 2, 3, 2, 4];
let threshold = 2;
let mut i = 0;
while i < nums.len() {
if nums[i] == threshold {
nums.remove(i); // 直接改 nums,没有闭包
} else {
i += 1;
}
}
println!("{nums:?}");
}输出:
[1, 3, 4]
⚠️ 陷阱:另一种常见形态是「闭包借用
&mut self的字段,同时调用self的另一个&mut self方法」。修法是把需要的字段先mem::take出来,或者把方法拆成 「先算后写」两步。绝对不要用unsafe绕过——这类冲突背后几乎总是真实的别名。
Cow<'a, T> 与 std::mem 工具箱
Cow<'a, T>:写时克隆
Cow(Clone on Write,写时克隆)是一个枚举,只有两个变体:
rust
pub enum Cow<'a, B>
where
B: ToOwned + ?Sized,
{
Borrowed(&'a B), // 借用,不动原数据
Owned(<B as ToOwned>::Owned), // 拥有,需要时才有
}API 设计例子:一个「有空格就替换成下划线」的规范化函数。大多数输入不需要改, 用 Cow 就能避免无意义的分配:
rust
use std::borrow::Cow;
// 返回 Cow<'_, str>:没改就返回借用,改了才返回新 String
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_")) // 只有这条路径会分配
} else {
Cow::Borrowed(input) // 零分配,直接借用输入
}
}
fn main() {
let clean = normalize("already_fine");
let dirty = normalize("needs fixing");
println!("clean = {clean}");
println!("dirty = {dirty}");
println!(
"clean 是 Borrowed = {}, dirty 是 Owned = {}",
matches!(clean, Cow::Borrowed(_)),
matches!(dirty, Cow::Owned(_))
);
// to_mut():需要修改时才克隆一份 Owned 出来
let mut c: Cow<'_, str> = Cow::Borrowed("hi");
c.to_mut().push('!'); // 把 "hi" 克隆成 String 再 push
println!("改后 = {c}");
}输出:
clean = already_fine dirty = needs_fixing clean 是 Borrowed = true, dirty 是 Owned = true 改后 = hi!
常用方法:
| 方法 | 作用 |
|---|---|
Cow::Borrowed(&s) / Cow::Owned(String) | 构造两个变体 |
to_mut(&mut self) -> &mut Owned | 需要修改时才克隆,之后一直是 Owned |
into_owned(self) -> Owned | 转成拥有所有权,Borrowed 会克隆一次 |
as_ref() / Deref | 当 &str / &[T] 用,Cow<str> 可直接调 &str 方法 |
matches!(c, Cow::Borrowed(_)) | 判断当前是借用还是拥有 |
💡 对照:Java 的
String不可变、substring老版本共享底层数组(后来改了), C++ 的std::string_view只借用不能拥有。Cow把「借用优先、必要时拥有」 这件事变成了返回值类型的一部分,调用方一眼就知道「可能分配」。
std::mem 常用函数
rust
use std::mem;
fn main() {
// swap:交换两个同类型变量的值(原地,不经过临时变量)
let (mut a, mut b) = (1, 2);
mem::swap(&mut a, &mut b);
println!("swap 后 a = {a}, b = {b}");
// take:取出值,原地留下 T::default()(要求 T: Default)
let mut v = String::from("hello");
let taken = mem::take(&mut v);
println!("take = {taken}, 原位置 = {v:?}");
// replace:取出值,原地放入新值(不要求 Default)
let replaced = mem::replace(&mut v, String::from("new"));
println!("replace 返回 = {replaced}, 现在 = {v}");
// drop:按值接收并析构,用来提前结束生命周期
let s = String::from("bye");
mem::drop(s);
// println!("{s}"); // E0382:s 已经被移动
// size_of / align_of:编译期常量,用于 FFI、缓存对齐、序列化
println!("size_of::<[u32; 4]>() = {}", mem::size_of::<[u32; 4]>());
println!("size_of::<Option<Box<i32>>>() = {}", mem::size_of::<Option<Box<i32>>>());
println!("size_of::<Option<i32>>() = {}", mem::size_of::<Option<i32>>());
println!("align_of::<u64>() = {}", mem::align_of::<u64>());
}输出:
swap 后 a = 2, b = 1 take = hello, 原位置 = "" replace 返回 = , 现在 = new size_of::<[u32; 4]>() = 16 size_of::<Option<Box<i32>>>() = 8 size_of::<Option<i32>>() = 8 align_of::<u64>() = 8
| 函数 | 签名(简化) | 用途 | 安全? |
|---|---|---|---|
mem::swap | fn swap<T>(a: &mut T, b: &mut T) | 交换两个值 | ✅ 安全 |
mem::replace | fn replace<T>(dest: &mut T, src: T) -> T | 换出新值 | ✅ 安全 |
mem::take | fn take<T: Default>(dest: &mut T) -> T | 取出并留默认值 | ✅ 安全 |
mem::drop | fn drop<T>(_x: T) | 提前析构 | ✅ 安全 |
mem::size_of | const fn size_of<T>() -> usize | 栈上占用字节数 | ✅ 安全,编译期常量 |
mem::align_of | const fn align_of<T>() -> usize | 内存对齐 | ✅ 安全,编译期常量 |
mem::forget | fn forget<T>(t: T) | 跳过析构(泄漏) | ⚠️ 安全但危险 |
mem::transmute | unsafe fn transmute<Src, Dst>(src: Src) -> Dst | 位级重解释 | ❌ unsafe,几乎总有更好替代 |
mem::zeroed | unsafe fn zeroed<T>() -> T | 全零值 | ❌ unsafe,对含引用的类型是 UB |
mem::uninitialized | 已废弃(1.39 起弃用) | — | ❌ 永不使用,改用 MaybeUninit |
⚠️ 危险说明:
mem::forget。它是安全函数,但会让值的析构函数永不运行。 后果按类型分三档:
- 纯数据(
Vec、String、Box)→ 只是泄漏内存,程序其余部分仍然安全。- 锁守卫
MutexGuard→ 锁永远不释放,后续lock()死锁。unsafe类型的析构安全不变式(drop guard)→ 可能造成 UB。 所以forget只用于「故意把所有权交给别处」的极少数场景(如把它交给 C 侧的 析构函数);日常想提前释放用mem::drop,想转移所有权用move。
⚠️ 危险说明:
mem::transmute。位级重解释不做任何检查,长度不匹配、 对齐不匹配、把&T变成&mut T全是 UB。绝大多数需求都能用as转换、f64::to_bits、u32::from_le_bytes、bytemuckcrate 等安全替代。 本书正文不依赖unsafe;不得不写时请参考官方Nomicon。
ManuallyDrop<T> 一句话
ManuallyDrop<T> 是一个「关掉自动析构」的包装:它保证编译器不会自动 drop 内部值, 但你必须在合适的时机用 ManuallyDrop::drop(&mut x) 或 into_inner() 手动处理, 否则就是泄漏——典型用途是在 Drop 实现里精细控制字段的释放顺序,或把值交给 FFI。
与其他语言的对照
| 主题 | Java | C++ | Python | C# | Rust |
|---|---|---|---|---|---|
| 共享只读数据 | 普通引用(GC 管) | const shared_ptr<T>& | 普通对象引用 | 普通引用(GC 管) | Rc<T> / Arc<T> |
| 共享可变数据 | 直接改(GC 管) | shared_ptr<T> | 直接改(GIL + GC) | 直接改(GC 管) | Rc<RefCell<T>> / Arc<Mutex<T>> |
| 打破循环 | 让 GC 处理 | weak_ptr<T> | 分代 GC 兜底 | 让 GC 处理 | Weak<T> + upgrade() |
| 引用计数 | 无(可达性 GC) | shared_ptr 计数 | 计数 + 分代 GC | 无(可达性 GC) | Rc / Arc 自动 |
| 循环垃圾 | GC 自动回收 | 泄漏(需 weak_ptr) | GC 自动回收 | GC 自动回收 | 泄漏(需 Weak,安全但漏) |
| 内部可变性 | 无此概念 | mutable 成员 | 无此概念 | 无此概念 | Cell / RefCell / Mutex |
| 借用规则运行时化 | 无 | 无 | 无 | 无 | RefCell + panic |
| 线程安全共享 | AtomicReference/synchronized | shared_ptr + mutex | GIL(多进程绕开) | lock / Interlocked | Arc<Mutex<T>> / 原子类型 |
| 闭包捕获 | 捕获变量(effectively final) | [&] / [=] 显式 | 按名捕获(后绑定) | 捕获变量(可改外部局部) | 按使用方式推导 + move |
move 语义 | 无 | std::move(转移) | 无(一切皆引用) | 无 | move 闭包强制按值捕获 |
| 堆分配 | new(所有对象) | new / make_shared | 所有对象 | new(引用类型) | Box::new / Rc::new / Arc::new |
| 泄漏是否 UB | 否(GC 兜底) | 否 | 否 | 否 | 否(安全,但资源不释放) |
几个值得单独说的对比:
- Java 引用 vs
Rc/Arc:Java 里「共享可变对象」是默认行为,靠 GC 保证安全和回收; Rust 里这是要显式写出来的特殊选择(Rc<RefCell<T>>),并且运行期会为别名付出 panic 风险。Rust 的默认是「唯一所有权 + 静态借用检查」,共享是例外而非常态。 AtomicReferencevsArc<Mutex<T>>:AtomicReference<T>只保证引用本身的 原子替换,改内部状态仍需 CAS 循环或额外同步;Arc<Mutex<T>>直接把「共享 + 互斥」 组合成一个类型,锁的作用域由MutexGuard的 RAII 保证,不会忘记解锁。- Python 引用计数 vs
Rc:两者都是计数,但 CPython 额外有分代 GC 清扫循环垃圾, 所以obj.parent = parent; parent.child = obj最终会被回收。Rust 不会自动回收——Rc成环就永久泄漏直到进程退出。这既是缺点(需要Weak)也是优点(没有 GC 停顿, 时延可预测)。 - C# 闭包 vs Rust 闭包:C# 的 lambda 捕获的是变量本身(编译器把它们提升到 一个闭包类),所以能直接改外部局部变量;Rust 的闭包需要显式声明
FnMut, 并且外部变量与闭包之间仍然服从借用规则——想「多个闭包共享一个可变状态」就必须 上Rc<RefCell<T>>。
常见坑与编译错误
E0382 use of moved value / borrow of moved value(闭包场景)
rust
fn main() {
let s = String::from("hello");
let consume = move || s; // 闭包把 s 移进环境,并在调用时把它交出去 -> FnOnce
let a = consume();
let b = consume(); // ❌ 第二次调用
println!("{a} {b}");
}error[E0382]: use of moved value: `consume`
--> e0382_closure.rs:6:13
|
5 | let a = consume();
| --------- `consume` moved due to this call
6 | let b = consume(); // E0382: use of moved value
| ^^^^^^^ value used here after move
|
note: closure cannot be invoked more than once because it moves the variable `s` out of its environment
--> e0382_closure.rs:4:27
|
4 | let consume = move || s;
| ^
note: this value implements `FnOnce`, which causes it to be moved when called原因:闭包体 s 是「把 s 交出去」,所以这个闭包只实现 FnOnce,调用一次就消耗掉了自己。
修法:让闭包只借或复制,从而升级成 FnMut / Fn:
rust
fn main() {
let s = String::from("hello");
let borrow_it = || s.clone(); // 只借用 s,可以重复调用
println!("{} {}", borrow_it(), borrow_it());
let show = || println!("{s}"); // 同样只读借用 -> Fn
show();
show();
}输出:
hello hello hello hello
⚠️ 陷阱:这里如果给
borrow_it加上move,它就会拿走s的所有权, 后面的show再借用s就会报E0382: borrow of moved value。move是「拿走」, 不是「多一份」——两个闭包想同时用一个变量时,要么都不用move(都借用), 要么用Rc共享(见「Send/Sync与这些类型的关系」)。
E0507 cannot move out of ... which is behind a shared reference
rust
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let rc = Rc::new(vec![1, 2, 3]);
let inner: Vec<i32> = *rc; // ❌
let cell = RefCell::new(String::from("x"));
let borrowed = cell.borrow();
let owned: String = *borrowed; // ❌
println!("{inner:?} {owned}");
}error[E0507]: cannot move out of an `Rc`
--> e0507.rs:6:27
|
6 | let inner: Vec<i32> = *rc; // ❌
| ^^^ move occurs because value has type `Vec<i32>`,
| which does not implement the `Copy` trait
|
help: consider removing the dereference here
|
6 - let inner: Vec<i32> = *rc; // ❌
6 + let inner: Vec<i32> = rc; // ❌
|
help: consider cloning the value if the performance cost is acceptable
|
6 - let inner: Vec<i32> = *rc; // ❌
6 + let inner: Vec<i32> = rc.clone(); // ❌error[E0507]: cannot move out of dereference of `Ref<'_, String>`
--> e0507.rs:10:25
|
10 | let owned: String = *borrowed; // ❌
| ^^^^^^^^^ move occurs because value has type `String`,
| which does not implement the `Copy` trait
|
help: consider cloning the value if the performance cost is acceptable
|
10 - let owned: String = *borrowed; // ❌
10 + let owned: String = borrowed.clone(); // ❌原因:*rc / *borrowed 背后是「共享引用」,共享引用不能把数据搬走 (否则其他共享者会看到被掏空的洞)。Rc<T> 与 Ref<'_, T> 都只给你 &T。
修法(按优先级):
rust
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let rc = Rc::new(vec![1, 2, 3]);
let view: &Vec<i32> = &rc; // 只读就用引用
println!("{view:?}");
let cloned: Vec<i32> = (*rc).clone(); // 真要一份独立的,clone
println!("{cloned:?}");
let mut rc2 = Rc::new(vec![1, 2, 3]);
if let Some(v) = Rc::get_mut(&mut rc2) {
// 确实只剩一个所有者时,零拷贝取回 &mut
v.push(4);
}
println!("{rc2:?}");
let cell = RefCell::new(String::from("x"));
let owned = cell.borrow().clone(); // RefCell 场景:clone 出来
let moved_out = cell.replace(String::new()); // 或用 replace 整块换出
println!("{owned} {moved_out}");
}输出:
[1, 2, 3] [1, 2, 3] [1, 2, 3, 4] x x
⚠️ 陷阱:
Cell<Vec<i32>>的get()会报T: Copy不满足;正确做法是take()(要求Default)或replace(Vec::new())。
RefCell 运行期 already borrowed: BorrowMutError
完整的 panic 输出(代码见「违反借用规则:运行期 panic 现场」,这里用 try_borrow_mut 版本,消息带类型名):
thread 'main' (12436) panicked at src/main.rs:11:32:
called `Result::unwrap()` on an `Err` value: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace原因:同一个 RefCell 上存在冲突的活跃借用。因为借用是运行期检查的, 编译器完全不知道这件事,所以错误只能在运行时暴露。
定位方法(按顺序做):
$env:RUST_BACKTRACE = "1"重跑,拿到精确行号(RefCell already borrowed的 panic 点 永远在borrow()/borrow_mut()/try_borrow*().unwrap()那一行)。- 在 panic 点的上文找还活着的
Ref/RefMut守卫变量。let x = data.borrow();这种写法会让x活到作用域结束;data.borrow()写在 表达式里则活到语句结束。 - 按「违反借用规则:运行期 panic 现场」的三种修法处理:缩短守卫寿命 / 克隆快照 / 换成
try_borrow*。 - 如果冲突是逻辑上必然的(比如遍历时改同一个
RefCell),说明数据结构设计错了—— 考虑改成双缓冲、Vec<Cell<T>>逐元素替换,或者换用Arc<Mutex<T>>让冲突表现为 「等待」而不是「panic」。
⚠️ 陷阱:
RefCell的 panic 不一定在同一条语句里成对出现。例如把RefMut存进结构体字段、或者return一个Ref,都会让守卫活得出乎意料地久。 让守卫尽量短命是使用RefCell的第一原则。
Rc 引用循环泄漏
症状:程序内存持续增长、Drop 从不执行、strong_count 永远不归零。代码和验证见「Rust 允许泄漏,泄漏不是 UB」。
原因:成环的强引用使计数永不归零。
修法:把环上的其中一条边改成 Weak。判断哪条边该弱: 「谁拥有谁」——拥有关系用 Rc,反向导航/回指用 Weak。 在父子树里,父拥有子(Rc),子回指父(Weak)。
验证:给节点加 Drop 打印,或在关键位置打印计数。下面是一个完整的「修好之后」的 自检程序——计数打印是排查泄漏时最有力的证据:
rust
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
name: &'static str,
next: RefCell<Option<Weak<Node>>>, // 唯一的区别:这里用 Weak 而不是 Rc
}
impl Node {
fn new(name: &'static str) -> Rc<Node> {
Rc::new(Node {
name,
next: RefCell::new(None),
})
}
}
impl Drop for Node {
fn drop(&mut self) {
println!("drop {}", self.name);
}
}
fn main() {
let a = Node::new("a");
let b = Node::new("b");
*a.next.borrow_mut() = Some(Rc::downgrade(&b));
*b.next.borrow_mut() = Some(Rc::downgrade(&a));
// 关键自检:每个节点的强引用数都应该是 1(只有局部变量),否则就是成环了
println!(
"a: strong = {}, weak = {}",
Rc::strong_count(&a),
Rc::weak_count(&a)
);
// 顺着 Weak 导航,用完必须处理 upgrade() 的 Option
let next = a.next.borrow().as_ref().and_then(Weak::upgrade);
println!("a.next = {}", next.as_ref().map_or("(已释放)", |n| n.name));
}输出:
a: strong = 1, weak = 2 a.next = b drop b drop a
如果修复正确,离开作用域时你会看到所有 Drop 都打印出来;如果 strong 大于 1, 说明环上还有 Rc 边没改成 Weak。
Arc<Mutex<T>> 死锁
死锁不是编译错误,而是运行期「卡住」。经典成因是同一线程对不可重入锁请求两次, 或者两个线程以相反顺序拿两把锁:
rust
use std::sync::{Arc, Mutex};
fn main() {
let a = Arc::new(Mutex::new(1));
let b = Arc::clone(&a);
let g1 = a.lock().unwrap();
println!("拿到第一把: {}", *g1);
let g2 = b.lock().unwrap(); // ❌ 同一线程再次 lock 同一把锁 -> 永久阻塞
println!("拿到第二把: {}", *g2);
}原因:std::sync::Mutex 是非重入锁。第一次 lock() 成功,第二次在同一个线程里 请求同一把锁,会无限等待自己释放。更常见的变体是两个线程以相反顺序拿两把锁:
rust
use std::sync::{Arc, Mutex};
use std::time::Duration;
fn main() {
let a = Arc::new(Mutex::new(1));
let b = Arc::new(Mutex::new(2));
let c = Arc::clone(&a);
let d = Arc::clone(&b);
let t1 = std::thread::spawn(move || {
let _g1 = a.lock().unwrap(); // 线程 1:先 a
std::thread::sleep(Duration::from_millis(50));
let _g2 = b.lock().unwrap(); // 再 b
});
let t2 = std::thread::spawn(move || {
let _g2 = d.lock().unwrap(); // 线程 2:先 b
std::thread::sleep(Duration::from_millis(50));
let _g1 = c.lock().unwrap(); // 再 a -> 循环等待
});
let _ = t1.join();
let _ = t2.join(); // 永远回不来
}⚠️ 注意:上面的程序会永久挂起,不要直接跑它,除非你准备好在终端里 Ctrl+C。 这类「加锁顺序不一致」的死锁是最难排查的一类——程序不崩溃、不报错,只是不动了。
修法一:用作用域块缩短守卫寿命(最直接)
rust
use std::sync::{Arc, Mutex};
fn main() {
let a = Arc::new(Mutex::new(1));
let b = Arc::clone(&a);
{
let g1 = a.lock().unwrap(); // 守卫只活在这个块里
println!("第一段: {}", *g1);
} // g1 在这里 drop -> 解锁
let g2 = b.lock().unwrap(); // 现在可以正常拿锁
println!("第二段: {}", *g2);
}输出:
第一段: 1 第二段: 1
修法二:先取出需要的值,再释放锁;不要跨锁调用别的函数
rust
use std::sync::{Arc, Mutex};
fn main() {
let shared = Arc::new(Mutex::new(vec![1, 2, 3]));
// 把要用的数据复制/克隆出来,守卫立刻结束
let sum: i32 = {
let guard = shared.lock().unwrap();
guard.iter().sum()
};
// 需要写时才重新拿锁
shared.lock().unwrap().push(sum);
println!("{:?}", shared.lock().unwrap());
}输出:
[1, 2, 3, 6]
多把锁的通用规则:全程序约定统一的加锁顺序(比如按变量名字典序), 或者干脆用一把锁保护整组数据,避免嵌套。
⚠️ 陷阱:
MutexGuard是!Send的,.await期间持锁在异步代码里会造成 跨线程持锁,编译器会用Send约束提示你。多线程/异步的完整死锁规避见〈并发编程〉。
闭包与 Drop
坑一:闭包捕获了值,闭包被 drop 时这些值才被 drop——顺序容易反直觉。
rust
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let outer = Noisy("外层变量");
let closure = move || {
// outer 已经被移进闭包的环境,随闭包一起释放
println!("闭包运行");
};
closure();
println!("闭包还在,但 outer 已经被移走了");
drop(closure); // 这里才 drop outer
println!("--- 结束 ---");
let _ = outer; // ❌ E0382:outer 已经 move 进闭包
}把最后一行删掉后输出:
闭包运行
闭包还在,但 outer 已经被移走了
drop 外层变量
--- 结束 ---坑二:Drop 类型不能被部分移动,也不能从字段里「抠」出内部值。
rust
struct Resource {
path: String,
handle: u32,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("释放 {}", self.path);
}
}
fn main() {
let mut r = Resource {
path: String::from("file.txt"),
handle: 7,
};
// ❌ E0509:cannot move out of type `Resource`, which implements the `Drop` trait
// let path = r.path;
// ✅ 用 mem::replace 把字段换出来,原位置留一个合法的新值
let path = std::mem::replace(&mut r.path, String::new());
println!("拿到 path = {path}");
// ✅ handle 是 Copy,直接复制没问题(复制不是移动)
let handle = r.handle;
println!("拿到 handle = {handle}");
// r 离开作用域时仍然会正常调用 Drop::drop 并释放字段
}输出:
拿到 path = file.txt 拿到 handle = 7 释放注意最后打印的是
释放(空串):因为Drop::drop读的是被replace成空的self.path,而真正的"file.txt"已经被移动到path里、由path自己析构。
⚠️ 陷阱:
Wrap { inner: Resource }里写let x = w.inner;同样报E0509, 因为Wrap的析构要访问inner,不允许它被单独搬走。给Wrap也加Drop也不行。 正确做法是给Wrap提供一个按值消费的方法fn into_inner(self) -> Resource—— 在方法内部用ManuallyDrop或先mem::replace出字段,再让self以「内部已被换空」 的状态析构。
E0373 closure may outlive the current function
rust
fn main() {
let mut s = String::from("hello");
let handle = std::thread::spawn(|| {
s.push_str(" world"); // ❌ 闭包借用了 s,但新线程可能活得更久
});
handle.join().unwrap();
println!("{s}");
}error[E0373]: closure may outlive the current function, but it borrows `s`,
which is owned by the current function
--> e0373.rs:4:37
|
4 | let handle = std::thread::spawn(|| {
| ^^ may outlive borrowed value `s`
5 | s.push_str(" world");
| - `s` is borrowed here
|
note: function requires argument type to outlive `'static`
help: to force the closure to take ownership of `s` (and any other referenced
variables), use the `move` keyword
|
4 | let handle = std::thread::spawn(move || {
| ++++原因:thread::spawn 要求闭包 'static(新线程不知道 main 的栈帧何时消失), 而闭包只借用了 s。
修法:加 move 把 s 的所有权交给闭包;如果多个地方还要用,就用 Arc:
rust
use std::sync::{Arc, Mutex};
fn main() {
let s = Arc::new(Mutex::new(String::from("hello")));
let for_thread = Arc::clone(&s);
let handle = std::thread::spawn(move || {
for_thread.lock().unwrap().push_str(" world");
});
handle.join().unwrap();
println!("{}", s.lock().unwrap());
}输出:
hello world
同样的 E0373 也会出现在「返回闭包」时(见「闭包作为参数与返回值」):函数返回 impl Fn 但闭包借用了 局部变量。修法同样是 move。
💡 对照:Java 里往线程里传
Runnable时捕获的局部变量必须是final/effectively final, 因为它会被复制到Runnable对象里;C# 直接提升变量(可变但共享的是同一个槽位)。 Rust 的move是显式的、可预测的,而且在编译期就告诉你「这里会移动」。
速查表
| 需求 | 写法 | 备注 |
|---|---|---|
| 打破递归类型 | Box<T> | 否则 E0072 |
| 运行期决定类型 | Box<dyn Trait> | 胖指针 16 字节 |
| 提前释放 / 释放锁守卫 | drop(x) | 等价 std::mem::drop;Copy 类型无效 |
| 单线程共享只读 | Rc::new(x) + Rc::clone(&x) | 用 Rc::clone 表明是加计数 |
| 多线程共享只读 | Arc::new(x) + Arc::clone(&x) | 原子计数,比 Rc 慢 |
| 单线程共享可变 | Rc<RefCell<T>> | 冲突运行期 panic |
| 多线程共享可变 | Arc<Mutex<T>> | 锁守卫 RAII 自动解锁 |
&self 改 Copy 小字段 | Cell<T> + get/set | 零开销,!Sync |
&self 改任意类型 | RefCell<T> + borrow_mut | 运行期借用检查 |
打破 Rc/Arc 环 | Weak::downgrade + upgrade() | 父持 Rc,子持 Weak |
| 计数检查泄漏 | Rc::strong_count / weak_count | 加 Drop 打印更直观 |
| 取回独占权 | Rc::try_unwrap / Rc::get_mut | 仅当 strong == 1 |
| 初始化一次的全局量 | OnceLock(1.70)/ LazyLock(1.80) | 都在 std,无需依赖 |
| 返回可能借用的字符串 | Cow<'_, str> | 不改就零分配 |
| 原地换出值 | std::mem::take / replace | take 要求 Default |
| 只读传参 | &T / &str / &[T] | 最优先,零成本 |
| 闭包只读环境 | Fn | 如 |x| x + 1 |
| 闭包改环境 | FnMut | 如累加器、计数器 |
| 闭包消费环境 | FnOnce | 只能调用一次 |
| 闭包活过当前函数 | move | 返回闭包、thread::spawn 必须 |
| 运行期选不同闭包 | Box<dyn Fn(..)> | 类型不同,只能装箱 |
| 攒返回值再改集合 | 先 collect 到新集合,再整体赋值 | 修闭包借用冲突的常用招 |