rust 闭包

本文详细介绍了Rust中的闭包概念,包括其定义、使用闭包简化代码、类型推导、作为函数返回值以及在结构体中的应用。重点讲解了FnOnce、FnMut和Fn特征的区别,以及闭包如何捕获作用域中的值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

闭包

闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值,例如:

fn main() {
   let x = 1;
   let sum = |y| x + y;

    assert_eq!(3, sum(2));
}

上面的代码展示了非常简单的闭包 sum,它拥有一个入参 y,同时捕获了作用域中的 x 的值,因此调用 sum(2) 意味着将 2(参数 y)跟 1(x)进行相加,最终返回它们的和:3。

可以看到 sum 非常符合闭包的定义:可以赋值给变量,允许捕获调用者作用域中的值。

使用闭包来简化代码
传统函数实现

想象一下,我们要进行健身,用代码怎么实现,这里是我的想法:

use std::thread;
use std::time::Duration;

// 开始健身,好累,我得发出声音:muuuu...
fn muuuuu(intensity: u32) -> u32 {
    println!("muuuu.....");
    thread::sleep(Duration::from_secs(2));
    intensity
}

fn workout(intensity: u32, random_number: u32) {
    if intensity < 25 {
        println!(
            "今天活力满满,先做 {} 个俯卧撑!",
            muuuuu(intensity)
        );
        println!(
            "旁边有妹子在看,俯卧撑太low,再来 {} 组卧推!",
            muuuuu(intensity)
        );
    } else if random_number == 3 {
        println!("昨天练过度了,今天还是休息下吧!");
    } else {
        println!(
            "昨天练过度了,今天干干有氧,跑步 {} 分钟!",
            muuuuu(intensity)
        );
    }
}

fn main() {
    // 强度
    let intensity = 10;
    // 随机值用来决定某个选择
    let random_number = 7;

    // 开始健身
    workout(intensity, random_number);
}

可以看到,在健身时我们根据想要的强度来调整具体的动作,然后调用 muuuuu 函数来开始健身。这个程序本身很简单,没啥好说的,但是假如未来不用 muuuuu 函数了,是不是得把所有 muuuuu 都替换成,比如说 woooo ?如果 muuuuu 出现了几十次,那意味着我们要修改几十处地方。

闭包实现
use std::thread;
use std::time::Duration;

fn workout(intensity: u32, random_number: u32) {
    let action = || {
        println!("muuuu.....");
        thread::sleep(Duration::from_secs(2));
        intensity
    };

    if intensity < 25 {
        println!(
            "今天活力满满,先做 {} 个俯卧撑!",
            action()
        );
        println!(
            "旁边有妹子在看,俯卧撑太low,再来 {} 组卧推!",
            action()
        );
    } else if random_number == 3 {
        println!("昨天练过度了,今天还是休息下吧!");
    } else {
        println!(
            "昨天练过度了,今天干干有氧,跑步 {} 分钟!",
            action()
        );
    }
}

fn main() {
    // 动作次数
    let intensity = 10;
    // 随机值用来决定某个选择
    let random_number = 7;
FnOnce,该类型的闭包会拿走被捕获变量的所有权。Once 顾名思义,说明该闭包只能运行一次:
    // 开始健身
    workout(intensity, random_number);
}

在上面代码中,无论你要修改什么,只要修改闭包 action 的实现即可,其它地方只负责调用,完美解决了我们的问题!

Rust 闭包在形式上借鉴了 Smalltalk 和 Ruby 语言,与函数最大的不同就是它的参数是通过 |parm1| 的形式进行声明,如果是多个参数就 |param1, param2,…|, 下面给出闭包的形式定义:

|param1, param2,...| {
    语句1;
    语句2;
    返回表达式
}
闭包的类型推导

与函数相反,闭包并不会作为 API 对外提供,因此它可以享受编译器的类型推导能力,无需标注参数和返回值的类型。
下面展示了同一个功能的函数和闭包实现形式:

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  ;

虽然类型推导很好用,但是它不是泛型,当编译器推导出一种类型后,它就会一直使用该类型:

let example_closure = |x| x;

let s = example_closure(String::from("hello"));
let n = example_closure(5);

首先,在 s 中,编译器为 x 推导出类型 String,但是紧接着 n 试图用 5 这个整型去调用闭包,跟编译器之前推导的 String 类型不符,因此报错:

error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |                             ^
  |                             |
  |                             expected struct `String`, found integer // 期待String类型,却发现一个整数
  |                             help: try using a conversion method: `5.to_string()`

结构体中的闭包

假设我们要实现一个简易缓存,功能是获取一个值,然后将其缓存起来,那么可以这样设计:

  • 一个闭包用于获取值
  • 一个变量,用于存储该值
    可以使用结构体来代表缓存对象,最终设计如下:
struct Cacher<T>
where
    T: Fn(u32) -> u32,
{
    query: T,
    value: Option<u32>,
}

特征 Fn(u32) -> u32 从表面来看,就对闭包形式进行了显而易见的限制:该闭包拥有一个u32类型的参数,同时返回一个u32类型的值。

接着,为缓存实现方法:

impl<T> Cacher<T>
where
    T: Fn(u32) -> u32,
{
    fn new(query: T) -> Cacher<T> {
        Cacher {
            query,
            value: None,
        }
    }

    // 先查询缓存值 `self.value`,若不存在,则调用 `query` 加载
    fn value(&mut self, arg: u32) -> u32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.query)(arg);
                self.value = Some(v);
                v
            }
        }
    }
}

上面的缓存有一个很大的问题:只支持 u32 类型的值,若我们想要缓存 &str 类型,显然就行不通了,因此需要将 u32 替换成泛型 E,该练习就留给读者自己完成,具体代码可以参考这里

捕获作用域中的值

在之前代码中,我们一直在用闭包的匿名函数特性(赋值给变量),然而闭包还拥有一项函数所不具备的特性:捕获作用域中的值。

fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

上面代码中,x 并不是闭包 equal_to_x 的参数,但是它依然可以去使用 x,因为 equal_to_x 在 x 的作用域范围内。

对于函数来说,就算你把函数定义在 main 函数体中,它也不能访问 x:

fn main() {
    let x = 4;

    fn equal_to_x(z: i32) -> bool {
        z == x
    }

    let y = 4;

    assert!(equal_to_x(y));
}

报错如下:

error[E0434]: can't capture dynamic environment in a fn item // 在函数中无法捕获动态的环境
 --> src/main.rs:5:14
  |
5 |         z == x
  |              ^
  |
  = help: use the `|| { ... }` closure form instead // 使用
三种 Fn 特征
  1. FnOnce,该类型的闭包会拿走被捕获变量的所有权。Once 顾名思义,说明该闭包只能运行一次:
fn fn_once<F>(func: F)
where
    F: FnOnce(usize) -> bool,
{
    println!("{}", func(3));
    println!("{}", func(4));
}

fn main() {
    let x = vec![1, 2, 3];
    fn_once(|z|{z == x.len()})
}
  1. FnMut,它以可变借用的方式捕获了环境中的值,因此可以修改该值:
fn main() {
    let mut s = String::new();

    let update_string =  |str| s.push_str(str);
    update_string("hello");

    println!("{:?}",s);
}
  1. Fn 特征,它以不可变借用的方式捕获环境中的值 让我们把上面的代码中 exec 的 F 泛型参数类型修改为 Fn(&'a str):
fn main() {
    let mut s = String::new();

    let update_string =  |str| s.push_str(str);

    exec(update_string);

    println!("{:?}",s);
}

fn exec<'a, F: Fn(&'a str)>(mut f: F)  {
    f("hello")
}
闭包作为函数返回值

但是如果要使用闭包作为函数返回值,该如何做?

先来看一段代码:

fn factory() -> Fn(i32) -> i32 {
    let num = 5;

    |x| x + num
}

let f = factory();

let answer = f(1);
assert_eq!(6, answer);

上面这段代码看起来还是蛮正常的,用 Fn(i32) -> i32 特征来代表 |x| x + num,非常合理嘛,肯定可以编译通过, 可惜理想总是难以照进现实,编译器给我们报了一大堆错误,先挑几个重点来看看:

fn factory<T>() -> Fn(i32) -> i32 {
  |                    ^^^^^^^^^^^^^^ doesn't have a size known at compile-time // 该类型在编译器没有固定的大小

Rust 要求函数的参数和返回类型,必须有固定的内存大小,例如 i32 就是 4 个字节,引用类型是 8 个字节,总之,绝大部分类型都有固定的大小,但是不包括特征,因为特征类似接口,对于编译器来说,无法知道它后面藏的真实类型是什么,因为也无法得知具体的大小。

同样,我们也无法知道闭包的具体类型,该怎么办呢?再看看报错提示:

help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/main.rs:11:5: 11:21]`, which implements `Fn(i32) -> i32`
  |
8 | fn factory<T>() -> impl Fn(i32) -> i32 {

嗯,编译器提示我们加一个 impl 关键字,哦,这样一说,读者可能就想起来了,impl Trait 可以用来返回一个实现了指定特征的类型,那么这里 impl Fn(i32) -> i32 的返回值形式,说明我们要返回一个闭包类型,它实现了 Fn(i32) -> i32 特征。

完美解决,但是,在特征那一章,我们提到过,impl Trait 的返回方式有一个非常大的局限,就是你只能返回同样的类型,例如:

fn factory(x:i32) -> impl Fn(i32) -> i32 {

    let num = 5;

    if x > 1{
        move |x| x + num
    } else {
        move |x| x - num
    }
}

运行后,编译器报错:

error[E0308]: `if` and `else` have incompatible types
  --> src/main.rs:15:9
   |
12 | /     if x > 1{
13 | |         move |x| x + num
   | |         ---------------- expected because of this
14 | |     } else {
15 | |         move |x| x - num
   | |         ^^^^^^^^^^^^^^^^ expected closure, found a different closure
16 | |     }
   | |_____- `if` and `else` have incompatible types
   |

嗯,提示很清晰:if 和 else 分支中返回了不同的闭包类型,这就很奇怪了,明明这两个闭包长的一样的,好在细心的读者应该回想起来,本章节前面咱们有提到:就算签名一样的闭包,类型也是不同的,因此在这种情况下,就无法再使用 impl Trait 的方式去返回闭包。

只需要用 Box 的方式即可实现:

fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
    let num = 5;

    if x > 1{
        Box::new(move |x| x + num)
    } else {
        Box::new(move |x| x - num)
    }
}

至此,闭包作为函数返回值就已完美解决,若以后你再遇到报错时,一定要仔细阅读编译器的提示,很多时候,转角都能遇到爱。

<think>好的,用户之前问了关于Rust字符串分割的问题,现在又问到了Rust闭包的进阶内容。我需要先回顾一下之前的回答结构,确保这次回答也保持类似的清晰结构。用户可能已经了解闭包的基础知识,所以现在需要深入一些高级特性。 首先,我应该明确闭包Rust中的定义和基本用法,但这次的重点是进阶部分。用户可能想了解闭包的类型、捕获环境的方式、作为参数和返回值使用,以及相关的trait比如Fn、FnMut、FnOnce的区别。 记得用户之前的问题得到了分步骤的解答,包括代码示例和关键点说明。这次也应该采用类似的结构,分块讲解不同的进阶主题,比如类型推断、环境捕获、trait约束、闭包作为参数和返回值,可能还有性能优化建议。 需要考虑到用户可能的实际应用场景,比如闭包在迭代器中的使用,或者在高阶函数中的使用。此外,用户可能对闭包的所有权和生命周期有疑问,特别是当闭包捕获变量时,移动和借用的情况。 还要注意闭包与函数指针的区别,以及如何将闭包作为trait对象处理。可能还需要提到move关键字的作用,强制闭包获取变量的所有权,这在多线程环境中尤其重要。 在代码示例方面,应该展示不同类型的闭包,比如使用Fn、FnMut的例子,以及闭包作为返回值的可能问题,比如需要使用Box来包裹。同时,要确保示例正确,避免常见的错误,比如生命周期问题或者类型不匹配。 用户可能还关心性能,比如闭包在运行时的大小和效率,所以可以提到闭包通常会被编译器优化,甚至内联,使得性能接近普通函数调用。 最后,总结部分需要强调闭包的灵活性和在Rust中的高效实现,帮助用户理解如何根据需求选择合适的闭包类型和捕获方式。还要提醒一些常见陷阱,比如在闭包中修改外部变量需要使用可变引用,或者闭包作为返回值时的生命周期管理。</think>以下是 Rust 闭包的进阶使用指南,分为核心概念、高级特性和实战技巧三个部分: --- ### 一、闭包类型系统(核心进阶) #### 1. 闭包的三种 Trait ```rust // Fn: 不可变借用环境变量 let add_x = |y| y + x; // 自动实现 Fn // FnMut: 可变借用环境变量 let mut x = 5; let mut add_x_mut = |y| { x += y; x }; // 自动实现 FnMut // FnOnce: 获取所有权 let consume_str = || String::from("hello"); // 自动实现 FnOnce ``` #### 2. 显式指定闭包类型 ```rust let square: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * x); ``` --- ### 二、环境捕获控制(关键进阶) #### 1. 强制所有权捕获(`move` 关键字) ```rust let data = vec![1, 2, 3]; let closure = move || { println!("Data length: {}", data.len()); // data 的所有权被移入闭包 }; ``` #### 2. 选择性捕获模式 ```rust let (a, mut b) = (10, 20); let closure = || { println!("a: {}", a); // 不可变借用 b += 1; // 可变借用 }; ``` --- ### 三、高阶函数应用(实战进阶) #### 1. 闭包作为参数 ```rust fn apply_twice<F>(mut f: F, x: i32) -> i32 where F: FnMut(i32) -> i32 { f(f(x)) } let result = apply_twice(|x| x * 2, 5); // 输出 20 ``` #### 2. 闭包作为返回值 ```rust fn make_adder(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n // 必须使用 move 来捕获 n } let add5 = make_adder(5); println!("{}", add5(3)); // 输出 8 ``` --- ### 四、生命周期管理(高级技巧) #### 1. 闭包中的引用 ```rust let s = String::from("hello"); let closure = || { println!("{}", s); // 闭包持有 &s }; // s 必须存活至闭包最后一次调用 ``` #### 2. 带生命周期的闭包参数 ```rust fn process<'a, F>(data: &'a str, f: F) where F: Fn(&'a str) + 'a { f(data) } ``` --- ### 五、性能优化要点 1. **静态分发优先**:优先使用泛型 + `impl Trait` 而非 trait 对象 ```rust fn call<F: Fn(i32) -> i32>(f: F) -> i32 { f(10) } ``` 2. **避免不必要的堆分配**:尽量不使用 `Box<dyn Fn...>` 3. **闭包内联优化**:Rust 编译器会自动内联简单闭包 --- ### 六、特殊场景处理 #### 1. 线程间传递闭包 ```rust use std::thread; let nums = vec![1, 2, 3]; thread::spawn(move || { // 必须使用 move println!("{:?}", nums) }).join().unwrap(); ``` #### 2. 闭包与迭代器链式调用 ```rust let sum: i32 = (1..100) .filter(|&x| x % 3 == 0) .map(|x| x * 2) .fold(0, |acc, x| acc + x); ``` --- ### 关键总结表 | 特性 | 适用场景 | 性能影响 | |--------------------|---------------------------------|--------------| | `Fn` | 只读访问环境变量 | 零开销 | | `FnMut` | 需要修改环境变量 | 微小运行时开销 | | `FnOnce` | 消耗环境变量 | 一次执行 | | `move` 关键字 | 跨线程/长期存储 | 所有权转移成本 | | 泛型约束 (`impl Trait`) | 高频调用场景 | 静态分发最优 | 掌握这些进阶用法后,可以写出更高效、更灵活的闭包代码,特别是在并发编程和泛型设计领域能发挥重要作用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

过去日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值