以下内容为本人学习过程中的记录笔记,其中可能存在不准确或错误,欢迎勘误及指正

在之前闭包的文章中,我们可以知道,在Rust中函数也可以像在Python、Nodejs一样作为参数传入到另一个函数中,具体使用时主要有两种方法来实现

闭包作为参数

在之前的文章中讲到闭包对捕获的变量所有权有三种特性:FnFnMutFnOnce,其分别表示了闭包在捕获环境时的不同方式。

Fn闭包作为参数

Fn:闭包通过不可变借用捕获环境中的变量,可以在多次调用中复用,不会改变捕获的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义一个函数,接受一个实现了 Fn 特性的闭包作为参数
fn apply_fn<F>(f: F, value: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(value)
}

fn main() {
// 使用一个简单的闭包,返回输入的两倍
let result1 = apply_fn(|x| x * 2, 5);
println!("Result1: {}", result1); // 输出 Result1: 10

// 使用闭包捕获环境,但不改变环境变量的值
let offset = 3;
// 使用一个闭包,返回输入加上捕获的环境变量
let result2 = apply_fn(|x| x + offset, 5);
println!("Result2: {}", result2); // 输出 Result2: 8
println!("factor: {}", factor); // factor的值没有改变
}

FnMut闭包作为参数

FnMut:闭包通过可变借用捕获环境中的变量,可以修改捕获的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn apply_fn_mut<F>(mut f: F, value: i32) -> i32
where
F: FnMut(i32) -> i32,
{
f(value)
}

fn main() {
// 使用 FnMut 闭包
let mut factor = 2;
let result1 = apply_fn_mut(
|x| {
factor *= 2;
x * factor
},
5,
);
println!("Result1 (FnMut): {}", result1); // 输出 Result1 (FnMut): 20
println!("factor: {}", factor); // 输出:factor: 4
}

运行上面的代码可以发现,变量factor的值发生了改变

FnOnce闭包作为参数

FnOnce:闭包通过值捕获环境中的变量,消耗捕获的变量,这种闭包只能调用一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn apply_fn_once<F>(f: F, value: i32) -> i32
where
F: FnOnce(i32) -> i32,
{
f(value)
}

fn main() {
// 变量factor是一个String类型,是一个非Copy类型
let factor = String::from("2");
// 使用move关键字强制按值捕获
let result2 = apply_fn_once(move |x| x * factor.parse::<i32>().unwrap(), 5);
println!("Result2 (FnOnce): {}", result2);
// 下面这行会编译错误,因为factor的所有权已经被移动
println!("{}", factor);
}

函数指针作为参数

通过以上的例子可以看出,使用闭包在非常方便的将函数作为参数传入到其他函数中(闭包也是一种函数),但使用闭包不容易编写逻辑性较为复杂的代码,否则可能会造成代码难以阅读,这时我们可以使用函数指针

函数指针作为参数

下面是一个最简单的函数指针作为参数的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
// apply函数接受一个函数指针f和一个整数x作为参数
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}

fn double(x: i32) -> i32 {
x * 2
}

fn main() {
let result = apply(double, 5);
println!("Result: {}", result); // 输出: Result: 10
}

多个函数指针参数

1
2
3
4
5
6
7
8
9
10
11
fn process(input: i32, f1: fn(i32) -> i32, f2: fn(i32) -> i32) -> i32 {
f2(f1(input))
}

fn double(x: i32) -> i32 { x * 2 }
fn square(x: i32) -> i32 { x * x }

fn main() {
let result = process(3, double, square);
println!("Result: {}", result); // 输出: Result: 36
}

函数指针作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
fn get_operation(op: &str) -> fn(i32, i32) -> i32 {
match op {
"add" => |x, y| x + y,
"multiply" => |x, y| x * y,
_ => |x, _| x,
}
}

fn main() {
let add_op = get_operation("add");
println!("Result: {}", add_op(5, 3)); // 输出: Result: 8
}

特征对象

结合之前Box<T>智能指针,可以更灵活的使用函数指针,比如下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let functions: Vec<Box<dyn Fn(i32) -> i32>> = vec![
// vector中的数据类型必须一致且大小需要在编译中确定
// 这里使用指针类型保证编译时类型和大小一致
Box::new(|x| x + 1),
Box::new(|x| x * 2),
Box::new(|x| x * x),
];

for f in functions.iter() {
println!("Result: {}", f(5));
}
}

以上只是一个非常基础的举例,事实上还有很多巧妙的用法,这里就不赘述了

总结

使用函数作为参数可以使程序更灵活、强大,可以提高代码的复用性和灵活性和实现回调机制等等优点,但在使用中也需要结合特定的场景来选择。比如需要考虑环境变量时,可能使用闭包更好用,但需要实现较为复杂的函数功能时,可能函数指针更加的方便,具体需要根据我们在实际编程中的使用场景来确定