Rust 编译器与运行时揭秘

第11章 闭包:匿名函数的编译器实现

作者 杨艺韬 · 10,881 字

第11章 闭包:匿名函数的编译器实现

“闭包不是魔法——它们是编译器帮你写的结构体。” —— 这是理解 Rust 闭包最核心的一句话。

本章要点

  • 每个闭包都会被编译器转化为一个唯一的匿名 struct,捕获的变量就是 struct 的字段
  • 三种捕获模式:不可变引用&T)、可变引用&mut T)、按值移动T
  • 编译器的捕获分析算法遵循最小权限原则——优先引用,必要时才 move
  • FnFnMutFnOnce 三个 trait 形成严格的层级关系,闭包实现哪个取决于它如何使用捕获的变量
  • move 关键字强制所有捕获变量按值捕获,但不改变闭包实现的 Fn trait 种类
  • 闭包的大小 = 所有捕获变量的大小之和(加 padding),不捕获任何变量的闭包是 ZST(零大小类型)
  • 不捕获变量的闭包可以被隐式转换为函数指针 fn()
  • Rust 闭包是零成本抽象——编译后的汇编与手写等价代码完全相同
  • Async 闭包将闭包与 async 机制结合,编译器为此引入了专门的 CoroutineClosure 处理路径

11.1 闭包的本质:编译器生成的匿名 struct

当你在 Rust 中写一个闭包时,编译器到底做了什么?很多语言(JavaScript、Python)的闭包通过运行时机制(堆分配、引用计数、垃圾回收)来捕获环境变量。Rust 的做法截然不同:编译器在编译期将每个闭包转化为一个匿名 struct 和对应的 trait 实现。没有堆分配,没有引用计数,没有运行时开销。

一个完整的例子

让我们从一个简单的闭包出发,完整展示编译器的转化过程:

// 你写的代码
fn main() {
    let name = String::from("Rust");
    let greeting = String::from("Hello");
    let greet = |suffix: &str| {
        println!("{}, {}{}!", greeting, name, suffix);
    };
    greet("!");
    greet("!!");
}

编译器会将这个闭包转化为一个匿名 struct 和 trait 实现。概念上等价于:

// 编译器做的事(概念等价,非实际生成的代码)
struct __closure_greet<'a> {
    greeting: &'a String,  // 只读 → 不可变引用
    name: &'a String,      // 只读 → 不可变引用
}

impl<'a> Fn<(&str,)> for __closure_greet<'a> {
    extern "rust-call" fn call(&self, (suffix,): (&str,)) -> () {
        println!("{}, {}{}!", self.greeting, self.name, suffix);
    }
}
// 因为 Fn: FnMut: FnOnce,编译器还会自动生成
// FnMut 和 FnOnce 的实现(委托给 Fn::call)

fn main() {
    let name = String::from("Rust");
    let greeting = String::from("Hello");
    let greet = __closure_greet { greeting: &greeting, name: &name };
    Fn::call(&greet, ("!",));
    Fn::call(&greet, ("!!",));
}

每个闭包都有唯一类型

一个极其重要的设计决策:每个闭包表达式都会产生一个独一无二的类型。即使两个闭包的签名完全相同,它们的类型也不同。这就是为什么闭包类型无法被直接写出来——你只能用 impl Fn(...) 或泛型约束来引用它。

let a = |x: i32| x + 1;
let b = |x: i32| x + 1;

// a 和 b 的类型不同!
// 你不能写 let c: ??? = a; 因为类型名是编译器内部生成的
// 只能通过 trait 约束来引用:
fn apply(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(x) }

这个设计有深远的性能意义:因为每个闭包类型是唯一的,编译器在单态化(monomorphization)时可以精确知道调用哪个函数,从而实现完全的内联优化

flowchart LR
    A["闭包表达式<br/><code>|x| x + captured</code>"] --> B["编译器分析"]
    B --> C["匿名 struct<br/>字段 = 捕获的变量"]
    B --> D["impl Fn/FnMut/FnOnce<br/>call 方法 = 闭包体"]
    B --> E["唯一类型名<br/>不可被源码引用"]
    B --> F["单态化<br/>调用点可内联"]

    style A fill:#3b82f6,color:#fff,stroke:none
    style B fill:#8b5cf6,color:#fff,stroke:none
    style C fill:#10b981,color:#fff,stroke:none
    style D fill:#10b981,color:#fff,stroke:none
    style E fill:#f59e0b,color:#fff,stroke:none
    style F fill:#ef4444,color:#fff,stroke:none

11.2 三种 Fn trait:闭包的调用协议

FnFnMutFnOnce 三个 trait 定义了闭包如何被调用,核心区别在于 self 的接收方式。它们在 library/core/src/ops/function.rs 中的真实定义:

// library/core/src/ops/function.rs(精简后的核心定义)

pub trait FnOnce<Args: Tuple> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
    //                               ^^^^
    //                               消耗 self —— 闭包被移动,只能调用一次
}

pub trait FnMut<Args: Tuple>: FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
    //                              ^^^^^^^^^
    //                              可变借用 self —— 可以多次调用,但需要独占访问
}

pub trait Fn<Args: Tuple>: FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
    //                          ^^^^^
    //                          不可变借用 self —— 可以多次、并发调用
}

注意几个重要细节:

  1. 继承链Fn: FnMut: FnOnce。实现了 Fn 的闭包自动实现 FnMutFnOnce
  2. extern "rust-call" ABI:参数被打包为元组传递,这是 Rust 内部的调用约定。
  3. Args: Tuple 约束:参数类型必须是元组,这使得闭包可以接受任意数量的参数。

trait 层级关系与子类型逻辑

继承关系 Fn: FnMut: FnOnce 的逻辑非常自然:

  • 如果你能通过 &self(不可变引用)调用闭包,那你肯定也能通过 &mut self(可变引用)调用——因为 &T 可以被升级为 &mut T 的使用场景。
  • 如果你能通过 &mut self 调用闭包,那你肯定也能通过 self(按值)调用——因为拥有所有权意味着你可以做任何事。
graph TB
    FnOnce["<b>FnOnce</b><br/><code>call_once(self)</code><br/>至少能调用一次<br/>消耗所有权"]
    FnMut["<b>FnMut: FnOnce</b><br/><code>call_mut(&amp;mut self)</code><br/>可以多次调用<br/>需要独占访问"]
    Fn["<b>Fn: FnMut</b><br/><code>call(&amp;self)</code><br/>可以多次并发调用<br/>最宽松的约束"]

    Fn -->|"继承"| FnMut
    FnMut -->|"继承"| FnOnce

    S1["消耗捕获变量的闭包<br/>例: || drop(name)<br/>只实现 FnOnce"]
    S2["修改捕获变量的闭包<br/>例: || count += 1<br/>实现 FnMut + FnOnce"]
    S3["只读捕获变量的闭包<br/>例: || println!(&quot;{}&quot;, x)<br/>实现 Fn + FnMut + FnOnce"]
    S4["不捕获变量的闭包<br/>例: |x| x + 1<br/>实现 Fn + FnMut + FnOnce"]

    S1 -.-> FnOnce
    S2 -.-> FnMut
    S3 -.-> Fn
    S4 -.-> Fn

    style FnOnce fill:#ef4444,color:#fff,stroke:none
    style FnMut fill:#f59e0b,color:#fff,stroke:none
    style Fn fill:#10b981,color:#fff,stroke:none
    style S1 fill:#fecaca,color:#333,stroke:none
    style S2 fill:#fef3c7,color:#333,stroke:none
    style S3 fill:#d1fae5,color:#333,stroke:none
    style S4 fill:#d1fae5,color:#333,stroke:none

标准库还为 &F&mut F 提供了 blanket 实现:如果 F: Fn,则 &F 也实现 Fn/FnMut/FnOnce;如果 F: FnMut,则 &mut F 也实现 FnMut/FnOnce。这使得闭包的引用也能直接被调用。

11.2.1 三 trait 完整签名对照表——把接收者/消耗/再调用彻底拆开

library/core/src/ops/function.rs 三 trait 的关键维度拉平一张表,是后续捕获分析、move 语义、async 闭包都会反复回到的锚点:

维度FnOnceFnMutFn
self 接收方式self(按值)&mut self(独占可变借用)&self(共享不可变借用)
方法名call_oncecall_mutcall
返回类型关联项type Output(定义在此)继承自 FnOnce继承自 FnOnce
可以调用的次数恰好 1 次(消耗 self)≥0 次(每次需要独占)≥0 次(可并发共享)
是否可以跨线程 Send取决于捕获字段是否全 Send同左同左
对应 ty::ClosureKindClosureKind::FnOnce(最强)ClosureKind::FnMutClosureKind::Fn(最弱)
格中位置(升级方向)终点(LATTICE_TOP)中间起点(LATTICE_BOTTOM)
典型闭包例子move || drop(s)|| count += 1|| println!("{x}")
能否隐式转换为 fn()否(FnOnce 不捕获也不一定)只有不捕获变量的 Fn
Box<dyn ...> 能否支持是(Rust 1.35+ 起完整支持)

格(lattice)视角的一个推论——11.4 节 process_collected_capture_informationclosure_kind 初始化成 LATTICE_BOTTOM = Fn 不是随手选的——它是”最乐观的假设”:开始时假定闭包只读、顺着每个 upvar 的使用方式做单调升格;格的偏序与 trait 的继承关系精确镜像Fn <: FnMut <: FnOnce,看起来反直觉的方向:子 trait 更”弱” = 格中更低)。升格只能向一个方向走,这保证了算法一次遍历就稳定收敛、不需要不动点迭代。

自动 trait 的独立维度——Send/Sync/Unpin/UnwindSafe 这四个 auto trait 与 Fn/FnMut/FnOnce 正交——一个 FnOnce 可以是 Send 也可以不是,取决于闭包 struct 字段(即捕获变量)整体是否满足。比如 move || println!("{}", rc.clone())rc: Rc<T>Send,所以整个闭包也不 Send,即使它只读。这正是 thread::spawn<F: FnOnce() + Send + 'static>Send 约束存在的必要性——只约束 Fn 族不够。

编译器如何选择 Fn trait

编译器选择 trait 的核心规则是:看闭包对捕获变量的最强操作

闭包中最强的操作实现的 traitself 类型可调用次数
不捕获变量 / 只读所有捕获变量Fn + FnMut + FnOnce&self无限次,可并发
修改某个捕获变量FnMut + FnOnce&mut self无限次,需独占
消耗某个捕获变量FnOnceself恰好一次

来看完整的代码示例:

// Fn:只读捕获的变量(通过 &self 调用)
let x = 10;
let fn_closure = || println!("{}", x);
fn_closure();  // 可以调用
fn_closure();  // 可以再次调用
// 甚至可以并发调用(因为 &self 允许共享)

// FnMut:修改捕获的变量(通过 &mut self 调用)
let mut count = 0;
let mut fn_mut_closure = || { count += 1; };
fn_mut_closure();  // count = 1
fn_mut_closure();  // count = 2
// 可以多次调用,但不能并发

// FnOnce:消耗捕获的变量(通过 self 调用)
let name = String::from("Rust");
let fn_once_closure = || { drop(name); };
fn_once_closure();  // name 被 drop 了
// fn_once_closure();  // 编译错误!闭包已被消耗

11.3 捕获模式:编译器如何决定怎么捕获变量

三种捕获模式

编译器为每个被捕获的变量独立选择捕获模式。这三种模式直接对应闭包 struct 中字段的类型:

按不可变引用捕获(&T

当闭包只读取捕获的变量时:

// 你写的
let x = 42;
let read_x = || println!("{}", x);

// 编译器生成的(概念等价)
struct __closure_read_x<'a> {
    x: &'a i32,  // 不可变引用
}
// 实现 Fn trait

按可变引用捕获(&mut T

当闭包修改捕获的变量时:

// 你写的
let mut count = 0;
let mut increment = || { count += 1; };

// 编译器生成的(概念等价)
struct __closure_increment<'a> {
    count: &'a mut i32,  // 可变引用
}
// 实现 FnMut trait(不是 Fn,因为需要修改 self 中的字段)

按值移动捕获(T

当闭包消耗捕获的变量,或使用了 move 关键字时:

// 你写的
let name = String::from("Rust");
let consume = || { drop(name); };

// 编译器生成的(概念等价)
struct __closure_consume {
    name: String,  // 按值持有,所有权被转移
}
// 只实现 FnOnce(因为 drop 消耗了 name)

混合捕获与精确捕获

一个闭包可以对不同变量使用不同的捕获模式。编译器为每个变量独立选择:

let name = String::from("Alice");
let mut age = 30;

let birthday = || {
    println!("Happy birthday, {}!", name);   // name: &String(只读)
    age += 1;                                 // age: &mut i32(修改)
};
// struct { name: &String, age: &mut i32 } — 实现 FnMut

从 Rust 2021 edition 开始,编译器可以做字段级的精确捕获(RFC 2229)。例如,闭包 || p.x += 1 只会捕获 p.x,而不是整个 p。这个特性在编译器中通过 compute_min_captures 函数实现(后面详细分析)。

11.3.1 捕获模式完整对照表——从源码 upvar 使用痕迹到闭包 struct 字段类型

把 11.3 节的三种捕获模式和 11.4 节即将看到的四级借用格打成一张纵向对齐表,能够一次性吃掉”用户代码→rustc 内部分析→生成的 struct 字段”的全部 mapping:

用户代码中对 upvar 的动作ExprUseVisitor 回调UpvarCaptureapply_capture_kind_on_capture_ty 产出闭包 struct 字段类型触发 ClosureKind 升级到
只读(println!("{x}") / let _ = xborrow(ImmBorrow)ByRef(ImmBorrow)&'region Tx: &'a TFn(不升级)
内部借用需独占但不写(罕见:let r = &mut *x 嵌套)borrow(UniqueImmBorrow)ByRef(UniqueImmBorrow)&'region T(丢失独占信息,to_mutbl_lossyx: &'a TFnMut
写入(x += 1 / x.push(v) / *x = ymutate / borrow(MutBorrow)ByRef(MutBorrow)&'region mut Tx: &'a mut TFnMut
消耗所有权(drop(x) / f(x) 按值传参 / move 闭包 + 非 Copy)consumeByValueTx: TFnOnce
move 闭包 + Copy 类型consume(但源变量仍可用)ByValueT(值被拷贝)x: T不升级(只有 Copy 才走这条)
use 闭包(实验性,2024 nightly)特殊回调ByUseTx: TFnOnce(与 ByValue 同级)

表的关键读法——从左到右就是闭包捕获的完整数据流——用户写 || x += 1ExprUseVisitor 遍历时在这行触发 mutate(x) 回调,被 InferBorrowKind delegate 记录为 ByRef(MutBorrow)process_collected_capture_information 看到 MutBorrowclosure_kindFn 升格到 FnMutapply_capture_kind_on_capture_tyx 的类型 i32 包装成 &'a mut i32,最后塞进闭包 struct 作为一个字段——六个阶段全由前两列的数据驱动。理解这张表,就理解了 upvar.rs 2704 行的核心骨架。

两个易错点——

  1. UniqueImmBorrow 在类型层”降级”为 &T——apply_capture_kind_on_capture_ty 通过 to_mutbl_lossyUniqueImmBorrow 映射成 Mutability::Not,但 closure_kind 已经被升格到 FnMut——这是”独占性在 kind 上保留、在类型上丢弃”的精妙平衡:类型层只需要两态(可变/不可变),kind 层才关心是否独占。
  2. move + Copy 不触发 FnOnce——move || x + 1x: i32)生成的 struct 是 { x: i32 }(按值持有),但闭包仍是 Fn——因为 call 方法里 self.xCopy、读取不消耗、也不需要 &mut self。很多人误以为 move 一定产生 FnOnce,这是把”捕获方式”和”调用方式”混淆的最常见来源。

11.4 编译器中的捕获分析算法

现在让我们深入 rustc 的源码,看看编译器是如何分析闭包捕获的。整个过程分为多个阶段。

第一阶段:类型检查入口(closure.rs)

当编译器遇到闭包表达式时,入口函数是 check_expr_closurerustc_hir_typeck/src/closure.rs)。它的核心工作是:

  1. 从上下文推断闭包的签名和 kind(deduce_closure_signature
  2. 确定闭包的函数签名(sig_of_closure
  3. 创建闭包类型,其中 closure_kind_tytupled_upvars_ty 暂时都是类型变量
  4. 对闭包体进行类型检查

关键观察:在这个阶段,Fn/FnMut/FnOnce 的选择和捕获变量的类型都尚未确定——它们将在后续的 upvar 分析阶段被填入。

第二阶段:捕获信息收集(upvar.rs)

闭包体被类型检查之后,编译器进入 upvar 分析阶段。核心入口是 analyze_closure,它完成四步工作:

// compiler/rustc_hir_typeck/src/upvar.rs(精简)
fn analyze_closure(&self, ..., body: &'tcx hir::Body<'tcx>, capture_clause: hir::CaptureBy) {
    // 1. 使用 ExprUseVisitor 遍历闭包体,收集每个外部变量的使用方式
    let mut delegate = InferBorrowKind { fcx: &closure_fcx, closure_def_id, ... };
    euv::ExprUseVisitor::new(&closure_fcx, &mut delegate).consume_body(body);

    // 2. 处理收集到的捕获信息,推断闭包 kind (Fn/FnMut/FnOnce)
    let (capture_information, closure_kind, origin) = self
        .process_collected_capture_information(capture_clause, &delegate.capture_information);

    // 3. 计算最小捕获集合(Rust 2021 精确捕获)
    self.compute_min_captures(closure_def_id, capture_information, span);

    // 4. 统一类型变量——将推断结果填入之前创建的类型变量
    let final_upvar_tys = self.final_upvar_tys(closure_def_id);
    let final_tupled_upvars_type = Ty::new_tup(self.tcx, &final_upvar_tys);
    self.demand_suptype(span, args.tupled_upvars_ty(), final_tupled_upvars_type);
}

第三阶段:借用种类的格分析

process_collected_capture_information 是决定闭包 kind 的关键函数。它遍历所有捕获信息,根据捕获方式推断闭包的最终 kind:

// compiler/rustc_hir_typeck/src/upvar.rs(精简)
fn process_collected_capture_information(
    &self,
    capture_clause: hir::CaptureBy,
    capture_information: &InferredCaptureInformation<'tcx>,
) -> (InferredCaptureInformation<'tcx>, ty::ClosureKind, ...) {
    // 从最宽松的 Fn 开始
    let mut closure_kind = ty::ClosureKind::LATTICE_BOTTOM; // = Fn

    let processed = capture_information.iter().cloned().map(|(place, mut info)| {
        // 应用精度限制规则
        let (place, capture_kind) = restrict_capture_precision(place, info.capture_kind);
        let (place, capture_kind) = truncate_capture_for_optimization(place, capture_kind);

        // 根据捕获方式"升级"闭包 kind
        let updated = match capture_kind {
            ty::UpvarCapture::ByValue => match closure_kind {
                ty::ClosureKind::Fn | ty::ClosureKind::FnMut => {
                    // 按值捕获 → 升级到 FnOnce
                    (ty::ClosureKind::FnOnce, Some((usage_span, place.clone())))
                }
                ty::ClosureKind::FnOnce => (closure_kind, origin.take()),
            },
            ty::UpvarCapture::ByRef(ty::BorrowKind::Mutable | ty::BorrowKind::UniqueImmutable) => {
                match closure_kind {
                    ty::ClosureKind::Fn => {
                        // 可变引用 → 升级到 FnMut
                        (ty::ClosureKind::FnMut, Some((usage_span, place.clone())))
                    }
                    _ => (closure_kind, origin.take()),
                }
            },
            _ => (closure_kind, origin.take()),  // 不可变引用不改变 kind
        };

        closure_kind = updated.0;

        // 根据 capture_clause(move/ref)调整捕获方式
        let (place, capture_kind) = match capture_clause {
            hir::CaptureBy::Value { .. } => adjust_for_move_closure(place, capture_kind),
            hir::CaptureBy::Ref => adjust_for_non_move_closure(place, capture_kind),
        };

        info.capture_kind = capture_kind;
        (place, info)
    }).collect();

    (processed, closure_kind, origin)
}

这里有一个关键的设计:借用种类形成一个格(lattice)。编译器从最宽松的开始(Fn),随着分析每个捕获变量的使用方式,逐步”升级”到更严格的种类。升级路径是:

Fn → FnMut → FnOnce

一旦升级就不会降级——如果任何一个捕获变量需要按值移动,整个闭包就只能是 FnOnce

flowchart TD
    Start["开始分析<br/>closure_kind = Fn"] --> Loop["遍历每个捕获变量"]
    Loop --> Check{"变量的使用方式?"}
    Check -->|"只读 (&T)"| NoChange["closure_kind 不变"]
    Check -->|"可变引用 (&mut T)"| UpMut{"当前 kind?"}
    Check -->|"按值移动 (T)"| UpOnce{"当前 kind?"}

    UpMut -->|"Fn"| SetMut["closure_kind = FnMut"]
    UpMut -->|"FnMut/FnOnce"| NoChange2["closure_kind 不变"]

    UpOnce -->|"Fn/FnMut"| SetOnce["closure_kind = FnOnce"]
    UpOnce -->|"FnOnce"| NoChange3["closure_kind 不变"]

    NoChange --> More{"还有变量?"}
    NoChange2 --> More
    NoChange3 --> More
    SetMut --> More
    SetOnce --> More
    More -->|"是"| Loop
    More -->|"否"| End["确定最终 closure_kind"]

    style Start fill:#3b82f6,color:#fff,stroke:none
    style End fill:#10b981,color:#fff,stroke:none
    style SetMut fill:#f59e0b,color:#fff,stroke:none
    style SetOnce fill:#ef4444,color:#fff,stroke:none

第四阶段:最小捕获计算

compute_min_captures 是 Rust 2021 精确捕获的核心。以 upvar.rs 中的注释为例:

// 输入(收集到的所有捕获信息):
//   Place(s, [])         -> ByRef(ImmBorrow)    // println!("{s:?}")
//   Place(p, [Field(x)]) -> ByRef(MutBorrow)    // p.x += 10
//   Place(p, [Field(y)]) -> ByRef(ImmBorrow)    // println!("{}", p.y)
//   Place(p, [])         -> ByRef(ImmBorrow)    // println!("{p:?}")
//   Place(s, [])         -> ByValue             // drop(s)
//
// 输出(最小捕获集合):
//   s -> Place(s, [])  ByValue          // ByValue 胜出
//   p -> Place(p, [])  ByRef(MutBorrow) // 祖先合并,MutBorrow 胜出

算法逻辑:对于同一根变量的多个 Place,如果其中一个是另一个的祖先,则保留祖先,并将后代的捕获方式合并上去(取更强的)。编译器中的 UpvarCapture 枚举定义了三种捕获方式:ByValueByUse(use 闭包专用)、ByRef(BorrowKind)

11.4.1 “取更强的” 是什么意思——UpvarCapture 的全序与合并规则

上面说”取更强的”是个关键点但需要精确化。rustc 内部对 UpvarCapture 定义了一个明确的全序关系——这和 11.3 节讨论的 Fn/FnMut/FnOnce 三态格是两件事。打开 rustc_hir_typeck/src/upvar.rs:1264,源码注释直接写出了这个顺序

// according to the ordering ImmBorrow < UniqueImmBorrow < MutBorrow < ByValue
let mut max_capture_info = root_var_min_capture_list.first().unwrap().info;
for capture in root_var_min_capture_list.iter() {
    max_capture_info = determine_capture_info(max_capture_info, capture.info);
}

四个级别从弱到强:

  • ImmBorrow(最弱):共享借用 &T。多个闭包或同一闭包多次可以共存。
  • UniqueImmBorrow:独占但不可变的借用——一种”罕见中间态”,例如 &mut &mut T 情形下内层借用要独占但不需要可写。这个变体在 Rust 借用检查里主要内部使用、用户代码很少直接感知。
  • MutBorrow:可变借用 &mut T
  • ByValue(最强):按值捕获,所有权转移给闭包。

源码的合并函数 determine_capture_infoupvar.rs:2516)把这个顺序实现成一张精确的优先级表(line 2537-2538 源码注释):

We select the CaptureKind which ranks higher based the following priority order: (ByUse | ByValue) > MutBorrow > UniqueImmBorrow > ImmBorrow

注意比原注释多了一个 ByUse——这是 2024 年新增的变体、专门给”use 闭包”(一种实验性 closure-like 构造)用的、和 ByValue 同级。对普通用户层面 4 级顺序保持不变。

11.4.2 从 UpvarCapture 到具体 Rust 类型的最后一步

4 级捕获方式推断完、还要变成闭包 struct 字段的实际类型(T / &T / &mut T)。这一步是 apply_capture_kind_on_capture_tyupvar.rs:2059)——只有 10 行,但把”抽象 kind → 具体 type”的映射讲得一清二楚:

// upvar.rs:2059
fn apply_capture_kind_on_capture_ty<'tcx>(
    tcx: TyCtxt<'tcx>,
    ty: Ty<'tcx>,
    capture_kind: UpvarCapture,
    region: ty::Region<'tcx>,
) -> Ty<'tcx> {
    match capture_kind {
        ty::UpvarCapture::ByValue | ty::UpvarCapture::ByUse => ty,
        ty::UpvarCapture::ByRef(kind) => Ty::new_ref(tcx, region, ty, kind.to_mutbl_lossy()),
    }
}

三条规则:

  • ByValue / ByUse → 原类型 T(所有权转移到闭包 struct 字段)
  • ByRef(Mutable)&'region mut T
  • ByRef(Immutable | UniqueImmBorrow)&'region TUniqueImmBorrow 的”独占”信息在类型层不体现——这是 to_mutbl_lossy 名字里 lossy 的来源,合并成 Mutability::Not

这解释了为什么 11.5 节展示的闭包 struct 里、按共享借用的字段是 &T、按可变借用的是 &mut T、按值的是 T 本身——完全对应这 10 行函数。闭包最神秘的”编译器自动生成的 struct 长什么样”问题就卡在这一处 match 语句上——看懂这一处、闭包的所有物理表现都能推导出来。

这一处也是编译阶段和类型系统的交接点:捕获分析输出一个 UpvarCapture 值、类型系统拿到后用 apply_capture_kind_on_capture_ty 转成 Ty、最后这个 Ty 被填进闭包 struct 的字段定义。整个链路的因果从这里闭合——三阶段加起来就是”Rust 闭包能在完全静态分发下零成本工作”的全部秘诀。

11.5 闭包的内存布局

理解了编译器如何分析捕获之后,让我们看看闭包在内存中的实际布局。

基本布局规则

闭包 struct 的字段就是捕获的变量,布局遵循 Rust 的标准 struct 布局规则(包括对齐和 padding):

use std::mem::{size_of_val, align_of_val};

let a = 0u8;       // 1 字节
let b = 0u64;      // 8 字节
let c = 0u32;      // 4 字节
let s = String::from("hello");  // 24 字节(ptr+len+cap)

// 不捕获任何变量
let c0 = || 42;
println!("size={}, align={}", size_of_val(&c0), align_of_val(&c0));
// size=0, align=1 —— ZST!

// 捕获一个引用
let c1 = || println!("{}", a);
println!("size={}", size_of_val(&c1));  // 8 —— 一个指针

// 捕获两个引用
let c2 = || println!("{} {}", a, b);
println!("size={}", size_of_val(&c2));  // 16 —— 两个指针

// 按值捕获 String
let c3 = move || println!("{}", s);
println!("size={}", size_of_val(&c3));  // 24 —— String 的大小

// 混合捕获(引用 + 值)
let x = 42u32;
let y = String::from("world");
let c4 = move || println!("{} {}", x, y);
println!("size={}", size_of_val(&c4));
// 32 —— u32(4) + padding(4) + String(24) = 32
// 注意:编译器会对字段重排以优化布局

ZST 闭包:零大小的奇迹

不捕获任何变量的闭包是零大小类型(ZST)。这是一个非常重要的优化:

let c = |x: i32| x * 2;

assert_eq!(std::mem::size_of_val(&c), 0);

// ZST 意味着:
// 1. 不占用任何栈空间
// 2. 编译器知道调用点唯一对应一个函数
// 3. 可以完全内联

在迭代器链中,这个特性至关重要:

let sum: i32 = (0..1000)
    .filter(|x| x % 2 == 0)    // 闭包1:ZST(不捕获变量)
    .map(|x| x * x)             // 闭包2:ZST(不捕获变量)
    .sum();

// 整个链条中没有任何闭包占用内存。
// 编译器将所有闭包内联,生成一个紧凑的循环。

11.6 move 闭包:强制按值捕获

move 关键字将所有捕获的变量从引用模式改为按值模式。这是 Rust 中最容易被误解的特性之一。

move 不影响 Fn trait 种类

move 影响的是捕获方式,不是调用方式。 一个 move 闭包如果只读取捕获的变量,它依然实现 Fn

// 没有 move
let x = 42;
let c1 = || println!("{}", x);
// c1 的 struct: { x: &i32 }    大小:8 字节
// c1 实现 Fn

// 有 move
let x = 42;
let c2 = move || println!("{}", x);
// c2 的 struct: { x: i32 }     大小:4 字节
// c2 仍然实现 Fn!因为它只是读取 self.x,不需要 &mut self

编译器中的 move 处理

process_collected_capture_information 中,move 闭包的处理通过 adjust_for_move_closure 完成:

// 简化的逻辑
let (place, capture_kind) = match capture_clause {
    hir::CaptureBy::Value { .. } => {
        // move 闭包:所有引用捕获都变成按值捕获
        adjust_for_move_closure(place, capture_kind)
        // ByRef(ImmBorrow) → ByValue
        // ByRef(MutBorrow) → ByValue
        // ByValue → ByValue(不变)
    },
    hir::CaptureBy::Ref => {
        // 非 move 闭包:保持原样
        adjust_for_non_move_closure(place, capture_kind)
    },
};

但注意:move 只改变捕获方式,不改变闭包 kind 的推断。kind 的推断在 adjust_for_move_closure 之前就已经完成了。

move 的典型用途

1. 延长变量生命周期

最常见的用途是让闭包拥有捕获变量的所有权,从而让闭包能活得比原始变量更久:

fn spawn_greeting() -> impl Fn() {
    let name = String::from("Rust");
    // 没有 move 会编译失败:name 的引用活不过函数返回
    move || println!("Hello, {}!", name)
    // name 被移入闭包,闭包拥有 name 的所有权
}

2. 跨线程传递

std::thread::spawn 要求闭包实现 Send + 'static,通常需要 move

let data = vec![1, 2, 3];
std::thread::spawn(move || {
    println!("{:?}", data);  // data 被移入闭包,满足 'static 约束
});

3. Copy 类型的 move

对于 Copy 类型,move 实际上是复制而不是移动。let x = 42; let c = move || x; 之后 x 仍然可用。

11.7 闭包与函数指针:类型擦除与转换

fn() 与 Fn() 的区别

fn() 是函数指针类型,Fn() 是 trait。这两者有本质区别:

// fn() — 函数指针,固定大小(一个指针),没有环境
let fp: fn(i32) -> i32 = |x| x + 1;

// impl Fn() — trait 约束,每个闭包有自己的类型
fn apply(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(x) }

// dyn Fn() — trait 对象,通过 vtable 动态分发
fn apply_dyn(f: &dyn Fn(i32) -> i32, x: i32) -> i32 { f(x) }
类型大小分发方式能捕获环境?能内联?
fn(T) -> U8 字节(一个指针)间接调用难以内联
impl Fn(T) -> U闭包 struct 大小静态(直接调用)能内联
&dyn Fn(T) -> U16 字节(胖指针)动态(vtable)不能内联
Box<dyn Fn(T) -> U>16 字节(胖指针)动态(vtable)不能内联

闭包到函数指针的隐式转换

Rust 有一条特殊的转换规则:不捕获任何变量的闭包可以被隐式转换为函数指针

// 不捕获变量的闭包可以转换为 fn()
let closure = |x: i32| x * 2;
let fp: fn(i32) -> i32 = closure;  // 隐式转换

// 捕获了变量的闭包不能转换
let y = 10;
let closure_with_capture = |x: i32| x + y;
// let fp: fn(i32) -> i32 = closure_with_capture;  // 编译错误!

这个转换在编译器中是通过 coercion 机制实现的。当编译器检测到一个不捕获变量的闭包被用在期望 fn() 的地方时,它会插入一个 coercion,将闭包类型转换为函数指针。

这也是 ZST 闭包的一个有趣推论:因为不捕获变量的闭包是零大小的,它不需要任何”环境”数据,所以可以安全地退化为一个普通的函数指针。

dyn Fn 与 vtable

当你需要在运行时存储不同类型的闭包时,就需要使用 trait 对象(dyn Fn)。这引入了 vtable 间接调用:

// 静态分发:编译器知道具体类型,可以内联
fn call_static(f: impl Fn(i32) -> i32, x: i32) -> i32 {
    f(x)  // 直接调用,可内联
}

// 动态分发:通过 vtable 间接调用
fn call_dynamic(f: &dyn Fn(i32) -> i32, x: i32) -> i32 {
    f(x)  // 通过 vtable 查找函数指针,不可内联
}

// 在实际代码中的使用场景——存储多种不同的闭包
struct EventSystem {
    // 每个事件可以有不同的处理闭包
    handlers: Vec<Box<dyn Fn(&str)>>,
}

impl EventSystem {
    fn trigger(&self, event: &str) {
        for handler in &self.handlers {
            handler(event);  // 动态分发
        }
    }
}

Box<dyn Fn(...)> 的内存布局是两个指针:一个指向堆上的闭包 struct 数据,一个指向 vtable。vtable 中包含了 call 函数的指针、析构函数指针和大小/对齐信息。

11.8 闭包的零成本抽象:汇编级证明

Rust 声称闭包是”零成本抽象”——让我们用汇编来证明这一点。

对比实验:闭包 vs 手写代码

考虑这两段功能等价的代码:

// 版本1:使用闭包的迭代器链
pub fn sum_squares_closure(n: i32) -> i32 {
    (0..n).filter(|x| x % 2 == 0).map(|x| x * x).sum()
}

// 版本2:手写循环
pub fn sum_squares_manual(n: i32) -> i32 {
    let mut sum = 0;
    let mut i = 0;
    while i < n {
        if i % 2 == 0 {
            sum += i * i;
        }
        i += 1;
    }
    sum
}

使用 cargo build --release 编译后,两个版本生成的汇编完全相同——都是一个紧凑的循环,没有任何函数调用。编译器完成了:单态化(泛型参数具体化为闭包唯一类型)、内联(ZST 闭包的 call 方法被内联)、迭代器融合(整个链被融合为一个循环)、消除间接调用。

为什么是零成本的?

关键在于编译器的三个设计决策:

  1. 每个闭包类型唯一:编译器在单态化时精确知道调用哪个函数
  2. ZST 不占空间:不捕获变量的闭包不需要传递任何额外数据
  3. extern "rust-call" ABI:编译器控制调用约定,可以自由优化

使用 dyn Fn 时零成本不再成立——vtable 间接调用阻止了内联优化。在性能敏感的代码中,应优先使用泛型(impl Fn)。

11.9 闭包的存储与返回

闭包作为 struct 字段

存储闭包时,必须在静态分发和动态分发之间选择:

// 静态分发:零开销,但每个不同闭包产生不同的 Button 类型
struct Button<F: Fn()> { label: String, on_click: F }

// 动态分发:可以存储不同类型的闭包,但有堆分配+vtable开销
struct ButtonDyn { label: String, on_click: Box<dyn Fn()> }

选择原则:闭包类型编译期确定且单一用泛型;需要运行时多态用 Box<dyn Fn>;只需临时借用用 &dyn Fn

返回闭包

// impl Trait:静态分发,可内联。move 是必须的——否则引用局部变量会悬空
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

// Box<dyn Fn>:动态分发,可以根据条件返回不同闭包
fn make_op(op: &str) -> Box<dyn Fn(i32, i32) -> i32> {
    match op {
        "add" => Box::new(|a, b| a + b),
        "mul" => Box::new(|a, b| a * b),
        _ => Box::new(|_, _| 0),
    }
}

11.10 Async 闭包:闭包与异步的结合

Rust 1.85 (2025年2月) 稳定了 async closures,这是闭包机制与 async/await 的深度融合。

在 async 闭包之前,|url| async move { reqwest::get(&url).await } 实际上是一个返回 Future 的同步闭包。每次调用都创建新的 async block,需要独立捕获变量,无法优雅地引用环境。

async 闭包的语法和语义

// 新语法:async closure
let fetch = async |url: String| {
    reqwest::get(&url).await
};

// 可以多次调用
fetch("https://example.com".into()).await;
fetch("https://rust-lang.org".into()).await;

编译器中的实现

在编译器中,async 闭包通过 CoroutineClosure 来表示。在 check_expr_closure 中有专门的处理路径:

// compiler/rustc_hir_typeck/src/closure.rs(简化)
match closure.kind {
    hir::ClosureKind::Closure => {
        // 普通闭包:直接生成 Closure 类型
        Ty::new_closure(tcx, expr_def_id, closure_args.args)
    }
    hir::ClosureKind::CoroutineClosure(kind) => {
        // async 闭包:生成 CoroutineClosure 类型
        // 这个类型包含额外的信息:
        // - closure_kind_ty: Fn/FnMut/FnOnce
        // - coroutine_captures_by_ref_ty: 内部协程引用捕获的类型
        // - signature_parts_ty: 包含 resume_ty, yield_ty, return_ty
        Ty::new_coroutine_closure(tcx, expr_def_id, closure_args.args)
    }
    hir::ClosureKind::Coroutine(kind) => {
        // 协程(async block, gen block 等)
        Ty::new_coroutine(tcx, expr_def_id, coroutine_args.args)
    }
}

Async 闭包的复杂性在于它需要同时处理两层结构:

  1. 外层闭包:捕获环境变量
  2. 内层协程:async 执行体

当外层闭包被调用时,它创建一个内层协程。这个协程需要能够访问外层闭包捕获的变量。编译器需要确保:

  • 如果闭包是 Fn(可以被多次调用),内层协程应该借用外层闭包的捕获变量
  • 如果闭包是 FnOnce,内层协程可以移动外层闭包的捕获变量

AsyncFn trait 族

Fn/FnMut/FnOnce 对应,async 闭包有 AsyncFn/AsyncFnMut/AsyncFnOnce trait:

// 概念上的定义(实际实现更复杂)
trait AsyncFnOnce<Args> {
    type Output;
    async fn async_call_once(self, args: Args) -> Self::Output;
}

trait AsyncFnMut<Args>: AsyncFnOnce<Args> {
    async fn async_call_mut(&mut self, args: Args) -> Self::Output;
}

trait AsyncFn<Args>: AsyncFnMut<Args> {
    async fn async_call(&self, args: Args) -> Self::Output;
}

11.10.1 与 ch09 async 状态机的串联——两层结构的真实数据流

ch09(第 9 章《Async 状态机变换》)已经详细展开了 rustc_mir_transform/src/coroutine.rs 2011 行如何把 async fn / async { } 变换成状态机——那里的主角是单层协程:一个 async fn 捕获参数→生成状态机 struct→每个 .await 点成为一个状态。本章 §11.10 的 async 闭包引入了两层结构,两层之间的数据流正是 ch09 没展开的部分——这里补上。

两层结构的三种组合——

flowchart TB
    subgraph Outer["外层闭包(11章捕获分析的产物)"]
        O1["闭包 struct<br/>{ captured_upvars }"]
        O2["实现 AsyncFn / AsyncFnMut / AsyncFnOnce"]
    end

    subgraph Inner["内层协程(ch09 coroutine.rs 的产物)"]
        I1["协程状态机 struct<br/>{ state, locals, captured_from_outer }"]
        I2["实现 Coroutine trait<br/>poll → Future::poll"]
    end

    O2 -- "call(&self) 调用时<br/>创建 Future" --> I1
    O1 -- "通过 coroutine_captures_by_ref_ty<br/>把 upvar 引用传给协程" --> I1

    style O1 fill:#3b82f6,color:#fff,stroke:none
    style O2 fill:#3b82f6,color:#fff,stroke:none
    style I1 fill:#8b5cf6,color:#fff,stroke:none
    style I2 fill:#8b5cf6,color:#fff,stroke:none

关键桥接字段 coroutine_captures_by_ref_ty——这是 CoroutineClosure 独有的一个类型参数(见 §11.4 中 check_expr_closure 的 match 分支),在本章 §11.10 Ty::new_coroutine_closure 的参数列表里出现过、但没解释。它的作用正是两层之间的接口契约:

外层 AsyncFn traitself 接收方式协程捕获方式协程内部 upvar 类型语义
AsyncFn&self借用外层字段&'a T(重用外层引用)多次调用共享外层状态
AsyncFnMut&mut self独占借用外层字段&'a mut T多次调用独占外层状态
AsyncFnOnceself移动外层字段T(所有权转入协程)仅一次调用,吃光外层

和 ch09 §9.12.1 的对照——ch09 那一节列出 coroutine.rs 2011 行 = rustc 状态机变换的主体。本章 §11.12.1 的 6964 行 = 闭包基础设施。两者加起来 8975 行正是”rustc 处理可调用对象”的物理秤砣。进一步拆分责任:

  • ch09 负责”状态机形状”——每个 .await 切点分析、locals 的跨 await 持久化判定(across_yield 标志)、Pin/Unpin 不变量、discriminant 布局
  • 本章负责”可调用对象外壳”——upvar 收集、借用种类升格、捕获类型计算(apply_capture_kind_on_capture_ty)、Fn/FnMut/FnOnce/AsyncFn 等 trait 实现的自动生成
  • CoroutineClosure 是两者的交班处——外层闭包的 upvar 分析沿用本章的 upvar.rs、但多了一步”把 upvar 再次打包给内层协程”——这就是 coroutine_captures_by_ref_ty 的全部使命

一个具体例子看穿两层——

let mut counter = 0;
let mut bump = async || {
    counter += 1;                  // 修改外层 upvar
    tokio::task::yield_now().await; // 内层协程的 await 切点
    counter
};
let a = bump().await;  // 第一次调用:counter = 1
let b = bump().await;  // 第二次调用:counter = 2

数据流:(1) 外层 upvar 分析(本章 §11.4)看到 counter += 1、把闭包升格为 AsyncFnMut,捕获 counter&mut i32;(2) 每次 bump() 调用时,AsyncFnMut::async_call_mut(&mut self, ()) 执行——创建一个协程状态机(ch09 的产物),把 &mut self.counter 作为 upvar 传给协程;(3) 协程在 .await 处挂起时,&mut self.counter 必须活跃到协程 drop——这个生命周期由 coroutine_captures_by_ref_ty 在类型层保证;(4) 协程恢复、读取 self.counter 返回——状态机终止,外层 AsyncFnMut::async_call_mut 返回。两层 trait、两层 struct、一条数据流——这就是 async 闭包比普通闭包复杂一倍的根源。

11.11 编译器中的完整闭包处理流程

让我们总结编译器处理闭包的完整流程,从源码到最终的机器码:

flowchart TD
    A["源码中的闭包表达式<br/><code>|args| body</code>"] --> B["HIR lowering<br/>生成 hir::Closure 节点"]
    B --> C["类型检查入口<br/>check_expr_closure()"]
    C --> D["签名推断<br/>deduce_closure_signature()"]
    D --> E["闭包体类型检查<br/>check_fn()"]
    E --> F["捕获分析入口<br/>analyze_closure()"]
    F --> G["ExprUseVisitor<br/>遍历闭包体,收集变量使用信息"]
    G --> H["process_collected_capture_information<br/>推断 ClosureKind (Fn/FnMut/FnOnce)"]
    H --> I["compute_min_captures<br/>计算最小捕获集合(Rust 2021 精确捕获)"]
    I --> J["final_upvar_tys<br/>确定每个捕获变量的最终类型"]
    J --> K["统一类型变量<br/>demand_suptype / demand_eqtype"]
    K --> L["MIR 构建<br/>将闭包 struct 的构造和方法调用转化为 MIR"]
    L --> M["单态化<br/>为每个闭包类型生成独立的代码"]
    M --> N["LLVM 代码生成<br/>内联优化,生成最终机器码"]

    style A fill:#3b82f6,color:#fff,stroke:none
    style F fill:#8b5cf6,color:#fff,stroke:none
    style H fill:#f59e0b,color:#fff,stroke:none
    style I fill:#f59e0b,color:#fff,stroke:none
    style N fill:#10b981,color:#fff,stroke:none

关键数据结构与借用格

编译器中涉及的核心数据结构:hir::Closure(HIR 层闭包表示)、ty::ClosureArgs(闭包类型参数,含 kind_ty/sig/upvars_ty)、ty::UpvarCapture(捕获方式枚举)、ty::CapturedPlace(完整捕获信息)、InferBorrowKind(ExprUseVisitor 的委托)。

借用种类形成格结构(upvar.rs 开头注释):ImmBorrow -> UniqueImmBorrow -> MutBorrow,对应 ClosureKind 的 Fn -> FnMut -> FnOnce。每个变量从最弱开始,逐步升级。

11.12 常见误区与陷阱

误区一:move 闭包只能调用一次

let name = String::from("Rust");
let greet = move || println!("Hello, {}!", name);
greet();   // 第一次调用
greet();   // 第二次调用——完全合法!
// move 只影响捕获方式,不影响 Fn trait
// 因为 println! 只读 name,所以 greet 实现 Fn

误区二:闭包总是比函数调用慢

// 通过泛型传递的闭包——零开销
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }
let result = apply(|x| x * 2, 21);
// 编译后完全等价于 let result = 21 * 2;

误区三:同签名的闭包可以互相赋值

let mut f = |x: i32| x + 1;
// f = |x: i32| x + 2;  // 编译错误!两个闭包是不同的类型

// 如果需要重新赋值,使用 trait 对象
let mut f: Box<dyn Fn(i32) -> i32> = Box::new(|x| x + 1);
f = Box::new(|x| x + 2);  // 可以,因为类型是 Box<dyn Fn(...)>

误区四:闭包捕获整个变量

Rust 2021 中,闭包只捕获使用到的字段。|| println!("{}", config.name) 只捕获 config.name,不影响 config.debug

误区五:闭包 upvar 的 Drop 顺序和源变量一致

闭包 struct 字段的 drop 顺序由字段在 struct 中的定义顺序决定,而不是源码中变量的声明顺序。编译器为了优化内存布局可能重排字段——Rust 2021 的 RFC 2229 明确规定:精确捕获下,drop 顺序可能与 edition 2018 不兼容,这是 disjoint_capture_drop_reorder lint 存在的原因。对持有锁、连接、文件句柄等需要精确释放时机的类型,不要依赖闭包内捕获变量的相对 drop 顺序——显式 drop(guard) 是更可靠的做法。

误区六:Box<dyn FnOnce> 不能被调用

早期 Rust(1.35 之前)确实不能直接调用 Box<dyn FnOnce> 里的闭包——因为 call_once(self) 要按值消耗 self,而 Box<dyn Trait> 不能直接解包。Rust 1.35 引入了 FnBox 的退役并添加了特殊支持:Box<dyn FnOnce> 现在可以直接 callBox<dyn FnMut>Box<dyn Fn> 同样支持。这一改动让”存储一个仅能调用一次的回调(如 oneshot 通道)“的常见模式终于干净——不再需要 Option<Box<dyn FnOnce>>::take() 的绕弯。

11.12.2 借用格完整状态机——从 ImmBorrow 到 ByValue 的所有合法升格路径

把 §11.4.1 提到的 4 级借用格(ImmBorrow < UniqueImmBorrow < MutBorrow < ByValue)画成状态机,可以一眼看清所有合法升格路径——这张图对 debug 闭包借用错误特别有用:

stateDiagram-v2
    [*] --> ImmBorrow: 初始态<br/>遇到只读使用
    ImmBorrow --> UniqueImmBorrow: 遇到内部独占借用<br/>(罕见嵌套场景)
    ImmBorrow --> MutBorrow: 遇到写入
    ImmBorrow --> ByValue: 遇到消耗 / move
    UniqueImmBorrow --> MutBorrow: 再遇到写入
    UniqueImmBorrow --> ByValue: 遇到消耗 / move
    MutBorrow --> ByValue: 遇到消耗 / move
    ByValue --> [*]: 终态<br/>不可再升格

    note right of ImmBorrow
        闭包 kind 保持 Fn
        struct 字段: &T
    end note
    note right of MutBorrow
        闭包 kind 升格到 FnMut
        struct 字段: &mut T
    end note
    note right of ByValue
        闭包 kind 升格到 FnOnce
        struct 字段: T
    end note

关键性质——

  1. 单调性——状态只能向右走,不能回退——这由 determine_capture_infomax 语义保证。算法一次遍历即收敛。
  2. 四级 → 三 kind 的映射——ImmBorrow + UniqueImmBorrow 都对应 Fn(kind 层二合一)、MutBorrow 对应 FnMutByValue(含 ByUse)对应 FnOnce——这就是为什么 ClosureKind 是三值而 BorrowKind 是四值。
  3. 精确捕获与升格的交互——Rust 2021 的 compute_min_captures 做完字段级拆分后,每个 Place(如 p.xp.y独立运行一次这个状态机。也就是说 || { p.x += 1; p.y.clone(); } 会产生两个捕获:p.x 升到 MutBorrowp.y 停在 ImmBorrow——整个闭包的 kind 是两者中较高的 FnMut,而 struct 有两个独立字段 { x: &mut X, y: &Y }。这就是为什么精确捕获能让原本”因为 p.ySend 就整体不 Send”的闭包变成”只捕获 p.x 所以可以 Send”——一个常见的 upgrade 2021 后意外修复的 bug。

11.12.1 实测:rustc 闭包基础设施 6964 行的真实分布

把本章贯穿讨论的 4 个文件 + 2 个相关文件实测——

文件角色
compiler/rustc_hir_typeck/src/upvar.rs2704本章 §11.4 主角——ExprUseVisitor 调用 + 借用种类格分析 + compute_min_captures 最小捕获算法(Rust 2021 字段级精确捕获)
compiler/rustc_hir_typeck/src/expr_use_visitor.rs1857本章 §11.4 第二阶段提到的 ExprUseVisitor 真身——遍历 HIR 表达式、把 consume / borrow / mutate / fake_read 四种使用方式回调给 callback——chapter 提了名字但没说这一个文件就 1857 行
compiler/rustc_hir_typeck/src/closure.rs1148§11.4 第一阶段——闭包类型检查入口、签名推断
compiler/rustc_type_ir/src/ty_kind/closure.rs666TyKind::Closure / TyKind::CoroutineClosure 抽象层(含 §11.10 Async 闭包的 CoroutineClosure enum 变体)
compiler/rustc_middle/src/ty/closure.rs493ClosureKind enum + UpvarCapture + ClosureArgsParts 等具体类型
compiler/rustc_passes/src/upvars.rs96早期 HIR 通行——只是 upvar 名字收集、不做借用分析
本章主题合计6964

两条值得记住的物理事实——

  1. upvar.rs 2704 + expr_use_visitor.rs 1857 = 4561 行——本章 §11.4 第二阶段+ 第三阶段(借用种类的格分析)+ 第四阶段(最小捕获)的全部代码——closure.rs 1148 行(类型检查入口)重 4 倍——印证 §11.3 揭示的”捕获分析比类型推断难一个量级”——表面看”自动决定 Fn/FnMut/FnOnce”是简单算法、实际是 4561 行的格点合并 + 借用种类升级 + Rust 2021 字段级 disjoint capture 算法
  2. expr_use_visitor.rs 1857 行不只是给闭包用——它是 rustc 借用检查/移动检查的通用 HIR 遍历器——闭包捕获分析是它最大的用户、但 borrowck 检查、unused_assignments lint、drop check 也都依赖它——是”多 pass 共用 visitor”的 rustc 范式

串联 ch09 §9.12.1 的 coroutine.rs 2011 行——本章 §11.10 提到的 CoroutineClosure 把闭包和协程结合——其实是 ch09 状态机变换额外多承担外层闭包结构展开——coroutine.rs + expr_use_visitor.rs + 本目录 6964 = rustc 处理 “可调用对象(闭包/async fn/协程)” 的全部 ~9000 行——是 Rust 函数式抽象的真实工程秤砣。

11.13 本章小结

本章深入探讨了 Rust 闭包的编译器实现。核心要点:

闭包的本质:每个闭包被编译为一个唯一类型的匿名 struct,捕获的变量成为字段,闭包体成为 Fn/FnMut/FnOnce trait 的方法实现。

捕获分析:编译器在 rustc_hir_typeck/src/upvar.rs 中通过 ExprUseVisitor 遍历闭包体,收集每个变量的使用方式,然后通过格(lattice)结构逐步”升级”借用种类。Rust 2021 引入了字段级精确捕获,通过 compute_min_captures 计算最小捕获集合。

Fn trait 层级Fn: FnMut: FnOnce 形成严格的继承链。编译器根据闭包对捕获变量的最强操作自动选择实现哪个 trait。move 关键字只影响捕获方式(引用 vs 值),不影响 trait 种类。

零成本抽象:通过唯一类型 + 单态化 + 内联的组合,闭包在编译后与手写代码生成完全相同的汇编。只有使用 dyn Fn 时才引入 vtable 间接调用的开销。

Async 闭包:将闭包机制与协程结合,编译器通过 CoroutineClosure 处理两层结构(外层闭包 + 内层协程),并引入了 AsyncFn/AsyncFnMut/AsyncFnOnce trait 族。

物理事实:rustc 闭包基础设施 6 文件 6964 行——upvar.rs 2704 + expr_use_visitor.rs 1857(rustc 借用/移动检查的通用 HIR 遍历器、闭包是最大用户)+ closure.rs 1148——印证’捕获分析比类型推断难一个量级’;串联 ch09 §9.12.1 coroutine.rs 2011 = rustc 处理”可调用对象(闭包/async fn/协程)“全部 ~9000 行。