Rust 闭包
Rust 中的闭包是一种匿名函数,它可以捕获并存储其所在环境中的变量。

闭包允许在定义作用域之外访问变量,并且可以在需要时将其移动或借用给闭包使用。
闭包在 Rust 中被广泛应用于函数式编程、并发编程和事件驱动编程等领域。
闭包与函数的区别
闭包和普通函数都能封装一段逻辑,但闭包多了一项核心能力——捕获外部环境中的变量。下表列出两者的主要差异:
| 特性 | 闭包 | 函数 |
|---|---|---|
| 匿名性 | 没有名称,通常赋值给变量 | 有固定的 fn 名称 |
| 环境捕获 | 可以捕获外部变量 | 不能捕获外部变量 |
| 定义方式 | |参数| 表达式 | fn 名称(参数) |
| 类型推导 | 参数和返回值类型通常可以省略 | 必须显式指定参数和返回值类型 |
| 存储与传递 | 可以作为变量、参数、返回值 | 同样支持 |
闭包的声明与调用
闭包的基本语法如下:
let closure_name = |参数列表| 表达式或语句块;
参数可以有类型注解,也可以省略——Rust 编译器会根据上下文推断它们。
实例
fn main() {
// 闭包:省略类型注解,编译器自动推断
let add_one = |x| x + 1;
println!("add_one(4) = {}", add_one(4)); // 输出: 5
// 闭包:显式标注参数和返回值类型
let multiply = |a: i32, b: i32| -> i32 { a * b };
println!("multiply(3, 7) = {}", multiply(3, 7)); // 输出: 21
// 多行闭包需要加大括号
let greet = |name: &str| {
let greeting = format!("Hello, {}!", name);
greeting
};
println!("{}", greet("runoob")); // 输出: Hello, runoob!
}
闭包的调用方式与普通函数完全一致,在变量名后加括号并传入参数即可。
捕获外部变量
闭包最核心的特性是能够捕获其所在作用域中的变量。这是闭包与普通函数的本质区别。
闭包可以通过三种方式捕获外部变量:
| 捕获方式 | 对应 Rust 语义 | 说明 |
|---|---|---|
| 按引用捕获 | 类似 &T | 默认行为,闭包借用变量,外部仍可使用 |
| 可变借用捕获 | 类似 &mut T | 闭包需要修改变量,闭包本身须声明为 mut |
| 按值捕获 | 类似 T | 使用 move 关键字,将变量所有权移入闭包 |
对于那些实现了
Copytrait 的类型,比如i32、bool,move并不会把变量“搬走”,而是直接拷贝出一份值,原来的外部变量依然可以继续使用。也正因为这样,想把所有权转移这件事演示清楚,通常要选String、Vec这类非Copy类型,效果才够直观。
按引用捕获
默认情况下,闭包以不可变引用的方式借用外部变量。借用结束后,外部作用域可以继续使用该变量。
实例
fn main() {
let text = String::from("runoob");
// 闭包按引用捕获 text,不获取所有权
let print_text = || println!("text = {}", text);
print_text(); // 输出: text = runoob
// 借用已结束,外部仍然可以使用 text
println!("外部仍可使用: {}", text);
}
可变借用捕获
如果闭包需要修改捕获的变量,Rust 会以可变引用的方式捕获。此时闭包本身必须声明为 mut。
实例
fn main() {
let mut counter = 0;
// 闭包以 &mut 方式捕获 counter,闭包本身也要声明为 mut
let mut inc = || {
counter += 1;
};
inc();
inc();
println!("counter = {}", counter); // 输出: counter = 2
}
按值捕获(move)
通过在闭包前添加 move 关键字,闭包会获取所捕获变量的所有权。所有权转移后,外部作用域将无法再使用该变量。
实例
fn main() {
let owned = String::from("RUNOOB");
// move 将 owned 的所有权转移进闭包
let take_owned = move || println!("owned = {}", owned);
take_owned(); // 输出: owned = RUNOOB
// 若取消下面这行注释,将编译报错:owned 的所有权已被移入闭包
// println!("{}", owned);
}
当需要将闭包传递到另一个线程或返回到作用域之外时,move 尤为常用——它确保闭包持有所需数据的所有权,不会出现悬垂引用。
闭包 Trait:Fn / FnMut / FnOnce
Rust 编译器会根据闭包捕获变量的方式,自动为闭包实现对应的 trait。理解这三个 trait 是使用闭包作为函数参数或返回值的关键。
| Trait | 捕获方式 | 可调用次数 | 典型场景 |
|---|---|---|---|
Fn | 不可变借用(&T) | 多次 | 只读访问捕获变量 |
FnMut | 可变借用(&mut T) | 多次 | 需要修改捕获变量 |
FnOnce | 获取所有权(T) | 仅一次 | 消耗捕获变量,之后不可再调用 |
三者的继承关系为:Fn 是 FnMut 的子 trait,FnMut 是 FnOnce 的子 trait。也就是说,一个实现了 Fn 的闭包,自动也实现了 FnMut 和 FnOnce。
注意:
move关键字只是强制获取所有权,并不意味着闭包一定是FnOnce。如果闭包体中并没有消耗捕获的值,那么即使加了move,闭包仍可能实现Fn(可多次调用)。
实例
fn main() {
let name = String::from("runoob");
// Fn 闭包:只读取,不修改,可多次调用
let greet = || println!("Hello, {}!", name);
greet(); // 第一次调用
greet(); // 第二调用,依然正常
// FnMut 闭包:需要修改捕获变量
let mut count = 0;
let mut increment = || {
count += 1;
count
};
println!("increment() = {}", increment()); // 输出: 1
println!("increment() = {}", increment()); // 输出: 2
// FnOnce 闭包:消耗捕获的变量,只能调用一次
let data = String::from("RUNOOB");
let consume = move || {
let _ = data; // 将 data 移出闭包环境,消耗了它
println!("data has been consumed");
};
consume();
// consume(); // 若取消注释,编译报错:FnOnce 闭包只能调用一次
}
闭包作为参数和返回值
闭包可以作为函数参数传入,也可以作为函数的返回值返回。这是闭包在实际开发中最常见的两种用法。
闭包作为参数
将闭包作为参数时,需要用泛型约束指定闭包的 trait 类型。以下示例中,参数 F 被约束为 Fn(i32) -> i32,表示接收一个 i32 参数并返回 i32 的闭包。
实例
// 定义一个函数,接受闭包作为参数
fn apply(val: i32, f: F) -> i32
where
F: Fn(i32) -> i32, // F 必须实现 Fn(i32) -> i32
{
f(val)
}
fn main() {
let double = |x| x * 2;
let result = apply(5, double);
println!("Result: {}", result); // 输出: Result: 10
// 也可以直接传入匿名闭包
let result2 = apply(3, |x| x + 100);
println!("Result2: {}", result2); // 输出: Result2: 103
}
闭包作为返回值
由于闭包的类型是匿名的,返回闭包时需要使用 impl Trait 或 Box 来描述返回类型。
使用 impl Fn 返回闭包
当返回的闭包类型在编译期可以确定时,使用 impl Fn 即可,无需堆分配。
实例
// 返回一个闭包,捕获参数 x 并与传入值相加
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
// 必须 move,否则 x 是局部引用,闭包返回后 x 已失效
move |y| x + y
}
fn main() {
let add_five = make_adder(5);
println!("5 + 3 = {}", add_five(3)); // 输出: 5 + 3 = 8
let add_ten = make_adder(10);
println!("10 + 2 = {}", add_ten(2)); // 输出: 10 + 2 = 12
}
使用 Box 返回闭包
当需要在运行时动态选择不同闭包返回时,使用 Box 将闭包分配到堆上。
实例
fn make_adder(x: i32) -> Box i32> {
Box::new(move |y| x + y)
}
fn main() {
let add_ten = make_adder(10);
println!("10 + 2 = {}", add_ten(2)); // 输出: 10 + 2 = 12
}
| 方式 | 分配位置 | 适用场景 |
|---|---|---|
impl Fn | 栈 | 编译期可确定闭包类型,性能更优 |
Box | 堆 | 运行时动态选择闭包,或需要跨函数存储闭包 |
常见应用场景
闭包在 Rust 的日常开发中随处可见,以下是几个最典型的应用场景。
迭代器中的闭包
闭包经常与迭代器方法配合使用,用于对集合元素进行批量处理。
实例
fn main() {
let nums = vec![1, 2, 3, 4, 5];
// map:对每个元素进行转换
let squared: Vec = nums.iter().map(|x| x * x).collect();
println!("平方: {:?}", squared); // 输出: [1, 4, 9, 16, 25]
// filter:筛选满足条件的元素
let even: Vec = nums.iter().filter(|x| *x % 2 == 0).collect();
println!("偶数: {:?}", even); // 输出: [2, 4]
// fold:将集合归约为单个值
let sum: i32 = nums.iter().fold(0, |acc, x| acc + x);
println!("求和: {}", sum); // 输出: 15
}
闭包与多线程
在多线程编程中,闭包常用于定义线程的执行体。move 关键字在此场景下几乎是必选的,因为它将数据的所有权移入新线程,避免跨线程引用失效。
实例
use std::thread;
fn main() {
let nums = vec![1, 2, 3, 4, 5];
// 为每个数字创建一个线程,move 将 num 的所有权移入线程
let handles: Vec<_> = nums.into_iter().map(|num| {
thread::spawn(move || {
num * 2
})
}).collect();
// 等待所有线程完成并收集结果
for handle in handles {
let result = handle.join().unwrap();
println!("Result: {}", result);
}
}
闭包与错误处理
闭包可以返回 Result 或 Option 类型,配合迭代器方法实现简洁的错误处理逻辑。
实例
fn main() {
let nums = vec![3, -1, 4, -5, 9];
// 使用闭包查找第一个正数
let first_positive = nums.iter().find(|&&x| x > 0);
match first_positive {
Some(&n) => println!("第一个正数: {}", n), // 输出: 第一个正数: 3
None => println!("没有正数"),
}
// 使用闭包过滤并转换,配合 Result 处理可能的错误
let results: Vec> = nums.iter().map(|&n| {
if n > 0 {
Ok(n * 10)
} else {
Err("负数无法处理")
}
}).collect();
for r in results {
match r {
Ok(v) => println!("成功: {}", v),
Err(e) => println!("错误: {}", e),
}
}
}
性能与生命周期
闭包的性能
Rust 的闭包是轻量级的。编译器会对闭包进行内联优化,使得闭包调用的开销接近于直接调用普通函数。闭包本身不引入额外的虚函数调用或堆分配(除非显式使用 Box)。
闭包与生命周期
闭包的生命周期,和它所捕获的变量是绑在一起的。Rust 的生命周期系统会把这件事盯得很紧:闭包绝不会比它捕获的任何变量“活”得更久——一旦闭包引用了某个局部变量,编译器就在编译期直接拦住你,避免把这个闭包返回到该变量作用域之外。
实例
fn main() {
let text = String::from("runoob");
// 正确:闭包在 text 的作用域内使用
let print_text = || println!("{}", text);
print_text(); // 输出: runoob
// 如果尝试返回这个闭包,编译器会报错:
// fn make_closure() -> impl Fn() {
// let text = String::from("runoob");
// || println!("{}", text) // 错误:text 的生命周期不够长
// }
// 解决方法:使用 move 将所有权移入闭包
}
完整示例
以下示例综合演示了闭包的声明、捕获外部变量、作为参数传递等核心用法。
实例
// 定义一个函数,接受闭包作为参数
fn apply_operation(num: i32, operation: F) -> i32
where
F: Fn(i32) -> i32,
{
operation(num)
}
fn main() {
let num = 5;
// 定义闭包:对数字进行平方运算
let square = |x| x * x;
// 将闭包作为参数传入函数
let result = apply_operation(num, square);
println!("Square of {} is {}", num, result); // 输出: Square of 5 is 25
// 也可以直接传入匿名闭包
let result2 = apply_operation(num, |x| x * x * x);
println!("Cube of {} is {}", num, result2); // 输出: Cube of 5 is 125
}
运行该程序,输出如下:
Square of 5 is 25
Cube of 5 is 125
总结
Rust 的闭包是一种强大的抽象,它提供了一种灵活且表达力强的方式来封装逻辑。
闭包可以捕获环境变量,并且可以作为参数传递或作为返回值返回。闭包与迭代器结合使用,可以方便地实现复杂的数据处理任务。
Rust 的闭包设计兼顾了安全性、性能和生命周期——编译器会在编译期确保闭包不会引用已失效的变量,并通过内联优化保证闭包调用的零开销抽象。
-
- 文章 · 软件教程 | 42分钟前 |
- Postman Console 怎么看请求实际发送内容:请求日志、变量展开与响应核对
- 391浏览 收藏
-
- 文章 · 软件教程 | 4小时前 |
- Chrome DevTools Performance 怎么录制长任务:Main 轨道、红色三角与回放核对
- 415浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | future
- 从一个带并发数限制的请求深入 Dart 的 Future
- 269浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | 强迫症
- Dart 3.7格式化工具“乱改代码”?强迫症必看
- 309浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | Android
- Dart 速通攻略(面向 Android 工程师)
- 444浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | Rust
- 假如Flutter用Rust,你也写不出更快的App
- 306浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | 其他
- 最新的 Dart sdk 安装教程
- 311浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | 其他
- Flutter路由3分钟学会
- 297浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | scroll
- flutter_easy_refresh在3.38.3配合NestedScrollView的注意要点。
- 445浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | 其他
- Kotlin vs Dart:当“优雅”变成心智负担,我选择了更简单的 Dart
- 445浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | 其他
- Flutter自定义组件: 为横向列表自定义“进度条”式滚动指示器
- 452浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | 其他
- Dart - 完全解剖await/async原理
- 129浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ljg-skills
- ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
- 5037次使用
-
- MELO音乐
- MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
- 4573次使用
-
- UniScribe
- UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
- 4521次使用
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 4779次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 4731次使用
-
- VS Code 怎么给 Go 项目配置测试任务:tasks.json 运行与结果验收
- 2026-07-09 501浏览
-
- Windows 11 如何开启 HEIF 图片支持
- 2026-05-31 501浏览
-
- TikTok用户画像与付费订阅变现方法
- 2026-05-27 501浏览
-
- 学信网学历翻译件申请方法
- 2026-05-27 501浏览
-
- Windows 11 24H2 更新失败0x80070005解决方法
- 2026-05-26 501浏览

Rust 所有权
