首页 / Rust 入门教程 / 闭包(Closures)

Rust 入门教程

闭包(Closures)

本教程共 78 篇 · 第 48 篇 · 更新于 2026-08-08 · 约 11 分钟阅读

RustRust 入门教程闭包匿名函数FnFnMutFnOncemove

本节目标:理解闭包是能捕获环境的匿名函数,掌握它的语法与类型推导,弄清它和普通函数的区别,以及 Fn/FnMut/FnOnce 三种捕获方式。

闭包这名字听着玄,其实就一句话:能”顺手”用到周围变量的匿名函数。它和函数像,但比函数多一项本事——捕获作用域里的值。

1-1 闭包是什么

看个小例子就懂:

fn main() {
    let x = 1;
    let sum = |y| x + y; // 闭包捕获了外面的 x

    assert_eq!(3, sum(2)); // 1 + 2 = 3
}

sum 是个闭包,它接收参数 y,却用到了作用域里的 x。调用 sum(2) 时,把参数 2 和捕获的 1 相加。这就是”闭包”:能赋值给变量、能当参数传、还能捕获调用者作用域里的值。

1-2 闭包语法

闭包用 || 包住参数,{} 包住函数体。参数写法 |参数|,多个就 |a, b|

|param1, param2| {
    语句1;
    返回表达式
}

只有一行返回表达式时,可以省略花括号和 return

let sum = |x, y| x + y; // 最后一行表达式就是返回值

对比一下函数和闭包:

fn add_one_v1(x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

第一行是函数,后三行是闭包,形式很接近。闭包体最后一行(没加分号)的值,就是返回值。

Note

let action = || {...}; 是把”闭包本身”赋给 action,不是调用结果。所以要像函数一样用 action() 去调用它;只有 action() 才执行。

1-3 类型推导:推一次就定了

函数必须标参数和返回值类型,因为函数是给外人用的 API。闭包一般不对外,所以能享受编译器的类型推导,不用标类型。但注意:推导出来后类型就固定了,不能又传这种又传那种

let example_closure = |x| x;

let s = example_closure(String::from("hello")); // 推导出 x 是 String
let n = example_closure(5); // 报错:之前推成 String 了,5 不兼容

第一次用 String 调用,编译器把 x 定为 String;第二次传 5 就类型不符。这和泛型不同——泛型每种类型各生成一份代码,闭包只锁一个类型。

1-4 闭包能捕获环境,函数不能

这是闭包最独特的能力。函数定义在哪都访问不到外面的局部变量:

fn main() {
    let x = 4;
    fn equal_to_x(z: i32) -> bool { z == x } // 报错:函数抓不到 x
    equal_to_x(4);
}

同样逻辑用闭包就成:

fn main() {
    let x = 4;
    let equal_to_x = |z| z == x; // 闭包捕获了 x
    assert!(equal_to_x(4));
}

编译器甚至会提示你”用闭包替代函数”。捕获环境要额外分配内存存这些值,所以函数没有这负担,闭包有——这是它的代价。

1-5 三种 Fn 特征:怎么捕获

闭包捕获变量有三种方式,对应三种 Fn 特征:

  • FnOnce:拿走捕获变量的所有权,所以只能调用一次;
  • FnMut:以可变借用捕获,能修改被捕获的值;
  • Fn:以不可变借用捕获,只能读不能改。

一个闭包实现哪种,取决于它怎么用捕获的变量,不是怎么捕获的。规则是:所有闭包都至少实现 FnOnce;没移走所有权的还实现 FnMut;不需要改值的还实现 Fn。所以只读的闭包三种都算,最灵活。

FnMut 的例子,闭包修改了外面的 s

fn main() {
    let mut s = String::new();
    let mut update = |str: &str| s.push_str(str); // 捕获可变借用
    update("hello");
    println!("{:?}", s);
}

因为闭包内部要改 s,所以闭包本身得声明成 mut update(注意:这是语言层的 mut 修饰符,和 FnMut 特征名不是一回事,但常搭配使用)。

1-6 move 关键字

默认情况下,闭包按”借用”的方式捕获变量。若想强制闭包拿走所有权,在参数前加 move

use std::thread;

fn main() {
    let v = vec![1, 2, 3];
    let handle = thread::spawn(move || {
        println!("向量是:{:?}", v);
    });
    handle.join().unwrap();
}

move 常用于闭包生命期比捕获变量长时(比如扔给另一个线程)。闭包拿了 v 的所有权,线程才能安全地用。

Tip

一个闭包实现了哪种 Fn,取决于它怎么使用捕获变量,而不取决于是否写了 movemove 只决定”怎么捕获”(拿所有权),用法照旧。

1-7 闭包作函数参数

闭包常作为参数传给别的函数。因为每种闭包类型都不同,无法用具体类型标注,得用 Fn 系列 trait 做约束:

fn apply<F>(f: F)
where
    F: Fn(i32) -> i32,
{
    println!("{}", f(5));
}

fn main() {
    let double = |x| x * 2;
    apply(double); // 10
}

F: Fn(i32) -> i32 表示:参数 f 是个闭包,接收 i32、返回 i32Fn 系列 trait 不仅对闭包有效,对普通函数也有效,所以传具名函数也行。

Warning

想用闭包当返回值,直接用 Fn(...) 作返回类型会报错(trait 没有固定大小)。要用 impl Fn(...)Box<dyn Fn(...)>。这块进阶内容先留个印象,知道”返回闭包得用 impl FnBox”即可。

1-8 闭包作为返回值与 Fn 特征推断

闭包可以被当作值传来传去,但要把它作为函数返回值就有个坑:闭包的类型是”匿名”的,你写不出它的名字。于是返回闭包时,通常得把它装进 Box,写成 Box<dyn Fn(...) -> ...>(动态分发):

fn make_adder(n: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |x| x + n)
}

编译器会根据闭包”怎么捕获环境变量”自动推断它实现 FnFnMut 还是 FnOnce:只是借用就用 Fn;需要修改就用 FnMut;把变量”吃掉”(move 进闭包且不再归还)就用 FnOnce。你一般不用手写这些 bound,让编译器推即可,只有在把闭包当参数/返回值时才显式标注。

Tip

记不住三者区别时,这样想:Fn 只读环境、FnMut 改环境、FnOnce 消耗环境。绝大多数读环境的闭包都是 Fn,最省心。

1-9 闭包如何捕获环境

闭包之所以比函数强,是因为它能”抓走”周围作用域里的变量。捕获方式分三种,对应前面说的 Fn / FnMut / FnOnce

  • 只借不可变引用(只读):闭包实现 Fn,最省心,且往往能 Copy
  • 借可变引用(要改):实现 FnMut,调用时会改动外部变量;
  • 把变量”搬”进自己体内(move 所有权):实现 FnOnce,调用后变量就归闭包所有了。
let mut total = 0;
let mut add = |x: i32| { total += x; }; // 捕获 total 的可变引用 -> FnMut
add(3);
add(5);
println!("{total}"); // 8

多数情况下让编译器自动推断即可;只有当你要把闭包当参数或返回值时,才需要显式写出 Fn / FnMut / FnOnce 约束。

Note

为什么有的闭包能 Copy、有的不能?关键看它怎么捕获:只读借用最轻量,可以到处拷贝;一旦动了或吞了外部变量,闭包就带上了”状态”,通常不能再简单复制。

1-10 小结

  • 闭包是能捕获环境的匿名函数,语法 |参数| 表达式
  • 闭包类型可推导,但推导后类型固定,不能混用;
  • 闭包能捕获周围变量,普通函数不行;
  • 三种捕获方式:FnOnce(拿所有权)、FnMut(可变借用)、Fn(不可变借用);
  • move 强制闭包拿走捕获变量的所有权;
  • 闭包作参数用 Fn 系列 trait 约束;作返回值用 impl FnBox<dyn Fn>

下一章看迭代器,它是闭包的最佳搭档,能让你用声明式的方式处理集合。