Skip to content

闭包与 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调用时接收捕获方式能调用几次例子
FnOnceself(按值)移动或消费捕获的变量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::entryOption::unwrap_or_else 的某些用法、或者同时把闭包存两份)会报 E0525expected 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

moveCopy 类型:如果捕获的变量是 Copyi32bool&Tu64 等), 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,但闭包会活得久危险,改成捕获 TArc<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_mutFnMut

迭代器适配器的方法签名决定了闭包需要哪个 trait:

方法闭包约束说明
mapFnMut(Self::Item) -> B每个元素调用一次,可维护内部状态
filterFnMut(&Self::Item) -> bool同上,但要返回 bool
foldFnMut(B, Self::Item) -> B累加器就是「闭包内部状态」
for_eachFnMut(Self::Item)就地消费
sort_byFnMut(&T, &T) -> Ordering比较器
unwrap_or_elseFnOnce() -> 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,正是为了允许这种「带状态的映射」。 如果约束成 Fnacc += 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::&lt;Option&lt;Box&lt;i32&gt;&gt;&gt;() = 8
size_of::&lt;Option&lt;i32&gt;&gt;() = 8
align_of::&lt;u64&gt;() = 8
函数签名(简化)用途安全?
mem::swapfn swap<T>(a: &mut T, b: &mut T)交换两个值✅ 安全
mem::replacefn replace<T>(dest: &mut T, src: T) -> T换出新值✅ 安全
mem::takefn take<T: Default>(dest: &mut T) -> T取出并留默认值✅ 安全
mem::dropfn drop<T>(_x: T)提前析构✅ 安全
mem::size_ofconst fn size_of<T>() -> usize栈上占用字节数✅ 安全,编译期常量
mem::align_ofconst fn align_of<T>() -> usize内存对齐✅ 安全,编译期常量
mem::forgetfn forget<T>(t: T)跳过析构(泄漏⚠️ 安全但危险
mem::transmuteunsafe fn transmute<Src, Dst>(src: Src) -> Dst位级重解释unsafe,几乎总有更好替代
mem::zeroedunsafe fn zeroed<T>() -> T全零值unsafe,对含引用的类型是 UB
mem::uninitialized已废弃(1.39 起弃用)❌ 永不使用,改用 MaybeUninit

⚠️ 危险说明:mem::forget。它是安全函数,但会让值的析构函数永不运行。 后果按类型分三档:

  1. 纯数据(VecStringBox)→ 只是泄漏内存,程序其余部分仍然安全。
  2. 锁守卫 MutexGuard → 锁永远不释放,后续 lock() 死锁
  3. unsafe 类型的析构安全不变式(drop guard)→ 可能造成 UB。 所以 forget 只用于「故意把所有权交给别处」的极少数场景(如把它交给 C 侧的 析构函数);日常想提前释放用 mem::drop,想转移所有权用 move

⚠️ 危险说明:mem::transmute。位级重解释不做任何检查,长度不匹配、 对齐不匹配、把 &T 变成 &mut T 全是 UB。绝大多数需求都能用 as 转换、f64::to_bitsu32::from_le_bytesbytemuck crate 等安全替代。 本书正文不依赖 unsafe;不得不写时请参考官方 Nomicon

ManuallyDrop<T> 一句话

ManuallyDrop<T> 是一个「关掉自动析构」的包装:它保证编译器不会自动 drop 内部值, 但你必须在合适的时机用 ManuallyDrop::drop(&mut x)into_inner() 手动处理, 否则就是泄漏——典型用途是在 Drop 实现里精细控制字段的释放顺序,或把值交给 FFI。


与其他语言的对照

主题JavaC++PythonC#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_ptrGC 自动回收GC 自动回收泄漏(需 Weak,安全但漏)
内部可变性无此概念mutable 成员无此概念无此概念Cell / RefCell / Mutex
借用规则运行时化RefCell + panic
线程安全共享AtomicReference/synchronizedshared_ptr + mutexGIL(多进程绕开)lock / InterlockedArc<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 的默认是「唯一所有权 + 静态借用检查」,共享是例外而非常态。
  • AtomicReference vs Arc<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 valuemove 是「拿走」, 不是「多一份」——两个闭包想同时用一个变量时,要么都不用 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 上存在冲突的活跃借用。因为借用是运行期检查的, 编译器完全不知道这件事,所以错误只能在运行时暴露。

定位方法(按顺序做):

  1. $env:RUST_BACKTRACE = "1" 重跑,拿到精确行号(RefCell already borrowed 的 panic 点 永远在 borrow() / borrow_mut() / try_borrow*().unwrap() 那一行)。
  2. 在 panic 点的上文找还活着的 Ref / RefMut 守卫变量。 let x = data.borrow(); 这种写法会让 x 活到作用域结束;data.borrow() 写在 表达式里则活到语句结束。
  3. 按「违反借用规则:运行期 panic 现场」的三种修法处理:缩短守卫寿命 / 克隆快照 / 换成 try_borrow*
  4. 如果冲突是逻辑上必然的(比如遍历时改同一个 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

修法:加 moves 的所有权交给闭包;如果多个地方还要用,就用 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::dropCopy 类型无效
单线程共享只读Rc::new(x) + Rc::clone(&x)Rc::clone 表明是加计数
多线程共享只读Arc::new(x) + Arc::clone(&x)原子计数,比 Rc
单线程共享可变Rc<RefCell<T>>冲突运行期 panic
多线程共享可变Arc<Mutex<T>>锁守卫 RAII 自动解锁
&selfCopy 小字段Cell<T> + get/set零开销,!Sync
&self 改任意类型RefCell<T> + borrow_mut运行期借用检查
打破 Rc/ArcWeak::downgrade + upgrade()父持 Rc,子持 Weak
计数检查泄漏Rc::strong_count / weak_countDrop 打印更直观
取回独占权Rc::try_unwrap / Rc::get_mut仅当 strong == 1
初始化一次的全局量OnceLock(1.70)/ LazyLock(1.80)都在 std,无需依赖
返回可能借用的字符串Cow<'_, str>不改就零分配
原地换出值std::mem::take / replacetake 要求 Default
只读传参&T / &str / &[T]最优先,零成本
闭包只读环境Fn|x| x + 1
闭包改环境FnMut如累加器、计数器
闭包消费环境FnOnce只能调用一次
闭包活过当前函数move返回闭包、thread::spawn 必须
运行期选不同闭包Box<dyn Fn(..)>类型不同,只能装箱
攒返回值再改集合collect 到新集合,再整体赋值修闭包借用冲突的常用招

内容以 rustc 1.98.1 · Rust 2024 edition 为基准