Rust进阶[part7]_闭包

Rust进阶[part7]_闭包

闭包概述

  • 闭包是一个可以捕获所在环境中的变量的匿名函数
  • 在Rust中,闭包通过||符号定义,可以像普通函数一样调用,但和函数不同,闭包可以访问外部作用域的变量

特点

  1. 匿名性:没有函数名,通常作为表达式使用
  2. 捕获环境:能自动捕获定义所在作用域中的变量(无需显式声明)
  3. 类型推断:参数和返回值类型可由编译器自动推断,无需显式标注
  4. 灵活语法:语法简洁,可根据复杂度调整写法(单行可省略{}return
  5. 实现trait:编译器会为闭包自动实现FnFnMutFnOnce trait,使其能作为参数传递

语法

基本语法:|参数列表| 代码块(返回值由最后一行表达式决定)

// 无参数闭包
let hello = || println!("Hello, closure!");
hello(); // 调用闭包

// 单参数闭包(省略类型标注)
let square = |x| x * x;
println!("{}", square(5)); // 输出:25

// 多参数闭包(显式标注类型,可选)
let add = |a: i32, b: i32| -> i32 { a + b };
println!("{}", add(2, 3)); // 输出:5

// 多行闭包(需用{}包裹)
let complex_calc = |x: i32| {
    let y = x * 2;
    y + 3 // 隐式返回
};
println!("{}", complex_calc(4)); // 输出:11

使用场景

函数的参数

闭包常作为参数传递给函数(如迭代器方法、异步任务等),实现灵活的逻辑注入

// 1. 结合迭代器使用
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // 输出:[2, 4, 6, 8, 10]

// 2. 自定义接受闭包的函数
fn apply<F>(f: F, value: i32) -> i32 
where 
    F: Fn(i32) -> i32  // 约束闭包类型
{
    f(value)
}

let result = apply(|x| x * 3 + 2, 5);
println!("{}", result); // 输出:17

捕获环境变量

可以捕获并使用其定义所在环境中的变量,捕获方式由编译器根据使用场景自动推断

// 1. 按引用捕获(&T):适用于只读访问
let message = String::from("Hello");
let print_msg = || println!("{}", message); // 捕获message的引用
print_msg(); // 输出:Hello
println!("{}", message); // 仍可使用message(未转移所有权)

// 2. 按可变引用捕获(&mut T):适用于修改变量
let mut count = 0;
let mut increment = || {
    count += 1; // 修改捕获的变量
    println!("Count: {}", count);
};
increment(); // 输出:Count: 1
increment(); // 输出:Count: 2

// 3. 按值捕获(T):适用于需要转移所有权的场景
let name = String::from("Alice");
let take_name = || {
    println!("Name: {}", name); // 捕获name的所有权
};
take_name(); 
// println!("{}", name); // 错误:name的所有权已被闭包捕获

闭包原理

  • 自动实现的函数类型:编译器会为每个闭包自动实现FnFnMutFnOnce中的一个或多个trait:

    • FnOnce:闭包消耗捕获的变量(所有权转移),因此只能调用一次
    • FnMut:闭包通过可变引用修改捕获的变量,可多次调用
    • Fn:闭包通过不可变引用访问捕获的变量,可多次调用(实现Fn的闭包自动实现FnMutFnOnce
  • 闭包的类型推断

    • 闭包的参数和返回值类型由编译器根据上下文自动推断
    • 同一闭包的类型是唯一的(即使签名相同,不同闭包类型也不同)
    • 若需存储闭包或作为返回值,需通过trait对象(如Box<dyn Fn(...) -> ...>)实现
  • 生命周期与闭包

    • 捕获引用的闭包,其生命周期受限于所捕获变量的生命周期
    • 若闭包作为返回值,需确保返回的闭包不捕获已销毁的变量引用

move关键字

在参数列表前使用move关键字,可强制闭包取得它所使用的环境值的所有权(忽略默认的引用捕获)

// 基础示例:转移所有权
let x = vec![1, 2, 3];
let equal_to_x = move |y| y == x; // x的所有权被闭包捕获
// println!("{:?}", x); // 错误:x的所有权已转移
let y = vec![1, 2, 3];
assert!(equal_to_x(y));

// 多线程场景:确保闭包拥有变量所有权(避免生命周期问题)
use std::thread;

let s = String::from("thread message");
thread::spawn(move || { // 必须用move转移s的所有权到子线程
    println!("{}", s);
}).join().unwrap();

练习

  1. 实现一个闭包,计算两个数的乘积并加上一个外部定义的偏移量(如offset = 10
  2. 使用闭包作为filter方法的参数,从整数列表中筛选出偶数
  3. move关键字创建一个闭包,在新线程中打印捕获的字符串
  4. 编写一个函数,接受两个闭包参数(一个计算平方,一个计算立方),并返回两个闭包结果的和

示例答案参考

// 练习1
let offset = 10;
let calc = |a: i32, b: i32| a * b + offset;
println!("{}", calc(3, 4)); // 输出:22(3*4+10)

// 练习2
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<_> = numbers.into_iter().filter(|x| x % 2 == 0).collect();
println!("{:?}", evens); // 输出:[2, 4, 6]

// 练习3
use std::thread;
let msg = String::from("hello from thread");
thread::spawn(move || {
    println!("{}", msg);
}).join().unwrap();

// 练习4
fn combine<F1, F2>(f1: F1, f2: F2, x: i32) -> i32
where
    F1: Fn(i32) -> i32,
    F2: Fn(i32) -> i32,
{
    f1(x) + f2(x)
}
let square = |n| n * n;
let cube = |n| n * n * n;
println!("{}", combine(square, cube, 2)); // 输出:4 + 8 = 12