Rust编程思想(四)———— 灵动的闭包
闭包已经不是函数,而是一个可以保存环境变量的函数,在现代的编程语言中已经是标配了,在Rust中,闭包需要注意的除了变量的捕获,还就是所有权问题了,变量一旦被闭包带走,那么这个变量将不能再被使用,主程序可能就有问题了。。
1. 闭包的定义(Fn闭包)
闭包使用 || 符号来定义,类似于匿名函数。闭包可以捕获环境中的变量,并在需要时进行调用
fn main() {
let subtract= |a, b| a - b;
let result = subtract (10, 3);
println!("The result is: {}", result);
}
在上述示例中,我们定义了一个名为 subtract的闭包,它接受两个参数 a 和 b,并返回它们的差。我们通过 subtract (10, 3) 调用闭包,并将结果打印出来。可以看出,闭包和普通函数一样,可以接受参数并返回值。
创建闭包的语法如下:
let closure = |param1, param2, ...|
{
// 闭包函数体
};
使用 |param1, param2, ...| 来声明闭包的参数列表,参数之间使用逗号分隔。如果没有参数,则保留 ||。闭包主体使用 { ... } 包裹,可以是单个表达式或复合语句块,如果是单个表达式可以省略{ }
fn main() {
// 无主体的单行闭包
let subtract= |a, b| a - b;
let mul = |x, y| { // 有主体的多行闭包
let result = x * y;
result
};
let result = subtract (10, 3); //7
let another = mul(3, 5); // 15
}
捕获变量
闭包最大的特色在于它能够捕获定义它的环境中的变量。所谓捕获,是指闭包在运行时能够访问并使用这些环境变量,而不是简单地复制一份值。
fn main() {
let num = 5;
let closure = |x| x+ num; // 捕获了 num 变量
println!("The closure is: {}", closure(3));
println!("The num is: {}", num);
}
需要注意的是,闭包只是借用了环境变量的所有权,而不是完全获取了所有权。因此,如果在环境中继续使用这个被捕获的变量,依然是合法的。
2. 闭包的类型
从闭包对所捕获变量的所有权权来分,可以分为在种,实现了以下 trait 之一(按灵活性递减顺序):
- Fn:不可变借用捕获环境
- FnMut:可变借用捕获环境
- FnOnce:获取所有权捕获环境,只能调用一次
默认的普通闭包是Fn,即不可变借用捕获环境,这样的闭包所捕捞的变量不可修改,如果要修改所捕捞变量,则需要使用FnMut闭包;如果要在闭包内取得所捕获变量的所有权,则需要使用 move 关键字,即实现了FnMut的闭包。
Fn闭包
闭包 Fn trait 描述了在闭包中只能使用不可变借用捕获环境变量。上面定义的闭包即为一个Fn闭包,闭包捕获的变量是只读的。
FnMut闭包
如果在闭包里需要修改所捕获的变量,则需要使用 FnMut trait,通过可变引用定义闭包。如:
let mut count = 0;
let mut increment = || {
count += 1; // 可变借用 count
println!("Count: {}", count);
};
increment(); // Count: 1
increment(); // Count: 2
FnOnce闭包(所有权与移动闭包)
上例中闭包只是借用了 num 的所有权,但有些情况下,我们可能需要将变量的所有权移动到闭包中。比如当捕获的是一个Box、Vec或其他拥有资源的类型时,闭包需要获取所有权,避免资源被提前释放。
使用 move 关键字,将 num 的所有权彻底转移到闭包中。
fn main() {
let num = Box::new(5);
let closure = move |x| *num + x;
let result = closure(3); // 8
println!("The result is: {}", result);
}
现在 num 的所有权已经转移到了闭包中,因此闭包可以安全地访问和使用 num。但是,在闭包外部继续使用 num 就是非法的了,因为所有权已经被移走。
总的来说,如果只是访问数据,闭包会自动借用环境变量;如果需要获取所有权,则需要使用 move 关键字显式转移所有权。
- 如果闭包使用 move 后要移动的是可复制类型的值(如 i32),那么就会复制该值,这样即使创建了闭包,后面仍然可以使用捕获的变量
- FnOnce闭包就是在闭包中将所捕获变量的所有权转移给了闭包,原来的变量存储空间被释放了,所以原来的变量将不能被再使用。
各种闭包对比
3. 闭包作为参数和返回值
闭包可以作为参数传递给其他函数,使代码变得更加灵活和抽象。
闭包其实在C/C++中就可以用函数指针来实现,也非常方便,而且函数指针也是强类型,可以作为参数传递,唯一的差异就是函数指针没有变量捕获,必须要通过参数来传入。
fn apply<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(x)
}
fn create_closure() -> impl Fn() {
let x = 5;
move || println!("The value of x is: {}", x)
}
fn main() {
//闭包作为参数
let result2 = apply(|y| y * y, 5); // 25
println!("The result2 is: {}", result2);
//闭包作为返回值
let closure = create_closure();
closure();
}
上例中, apply 函数接受一个闭包 f 和一个整数 x 作为参数,并在内部调用闭包 f(x)。where 限制表示 F 实现了特定的特型 F: Fn(i32) -> i32,表示 F 是一个接受 i32 参数,返回 i32 结果的闭包类型。
调用 apply 时,我们传入了一个平方闭包 |y| y * y 和值 5,因此最终返回结果是 25。create_closure 函数,它返回一个闭包。通过这种方式,我们可以在不同的上下文中使用闭包,实现代码的复用和灵活性。
更多的使用闭包的目的还是在回调函数中,把闭包当作函数的参数传递,这时就理解为C/C++中的回调函数了,使用起来非常方便。但变量的捕获又是一把双刃剑,尤其使用FnMut闭包时会改变变量,所以需要谨慎使用。一般情况下尽量使用Fn闭包。
参考
Rust中的闭包