Rust 函数与闭包
- 函数声明:参数列表,返回值列表
- 函数体构造
- 函数调用
- 闭包
函数声明
Rust 语言函数声明方法如下:
fn function_name(param1: type1, param2: type2, ...) -> return_type {
block
}
请看一个具体的例子:
fn fib(n: u32) -> u32 {
if n < 1 {
return 0;
} else {
return n + fib(n - 1);
}
}
assert_eq!(fib(0), 0);
assert_eq!(fib(1), 1);
assert_eq!(fib(4), 10);
assert_eq!(fib(5), 14);
这是一个典型的递归函数,展示了函数的定义与调用。
💡 assert_eq!
宏多用于测试,如果两个参数不相等,就会报错。
函数参数
接下来我们将展示更多的参数传递例子。
- 传值:
fn main() {
let a = 5;
println!("before: {}", a); // before: 5
let y = add_one(a);
println!("after: {}, y={}", a, y); // after: 5, y=6
}
fn add_one(x: i32) -> i32 {
x + 1
}
- 传递引用:
fn main() {
let a = 5;
println!("before: {}", a); // before: 5
let y = add_one(&a);
println!("after: {}, y={}", a, y); // after: 5, y=6
}
fn add_one(x: &i32) -> i32 {
x + 1
}
- 可变函数参数:
fn main() {
let x = 5;
let y = add_one(x);
println!("x={}, y={}", x, y); // x=5, y=6
}
fn add_one(mut x: i32) -> i32 {
x = x + 1;
x
}
- 传递可变引用:
fn main() {
let mut a = 5;
println!("before: {}", a); // before: 5
increase(&mut a);
println!("after: {}", a); // after: 6
}
fn increase(x: &mut i32) {
*x = *x + 1;
}
以上几种参数传递方式,你会觉得传值与传引用没啥区别,在某些情况下确实是这样,下面的例子揭示了他们的区别:
fn main() {
let name = String::from("Rust");
println!("before: {}", name);
let greeting = hello(name);
println!("{}", greeting);
println!("after: {}", name);
}
fn hello(name: String) -> String {
format!("Hello, {}", name)
}
编译会得到一个错误:
--> src\main.rs:6:27
|
2 | let name = String::from("Rust");
| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
3 | println!("before: {}", name);
4 | let greeting = hello(name);
| ---- value moved here
5 | println!("{}", greeting);
6 | println!("after: {}", name);
| ^^^^ value borrowed here after move
把它改造为传引用的版本:
fn main() {
let name = String::from("Rust");
println!("before: {}", name); // before: Rust
let greeting = hello(&name);
println!("{}", greeting); // Hello, Rust
println!("after: {}", name); // after: Rust
}
fn hello(name: &String) -> String {
format!("Hello, {}", name)
}
这时你可能有一连串的问题:
- 什么是
Copy trait
,什么是value moved
,什么是value borrowed
? - 为什么字符串不用
let name = "Rust";
,而要用let name = String::from("Rust");
?
这里先不先回答这些问题,一方面是给读者留下思考的空间,另一方面是我们会在接下来的笔记中讲述。
💡 我们对以上几种传参方式作一个建议:数字类型参数用传值的方法,其他类型的参数尽量用引用。
返回值
Rust 函数也是表达式,返回值的方法与之前学过的表达式类似:
fn add(x: i32, y: i32) -> i32 {
x + y
}
assert_eq!(add(1, 1), 2);
Rust 函数除了可以用 return expr;
返回值外,还可以在函数体最后用不带分号的表达式返回值。如果函数不显示返回值,可以不写返回值类型:
fn hello(name: &str) {
println!("Hello, {}", name);
}
hello("Rust");
💡 不显示返回值其实是返回 ()
。
闭包(Closures)
Rust 的闭包是匿名函数,闭包可以绑定到变量上,也可以作为其他函数的参数。闭包与函数类似,都封装了一定的功能,与函数不同的是,闭包能捕获环境变量,先看一个例子:
fn main() {
let color = String::from("green");
let print = || println!("`color`: {}", color);
print(); // `color`: green
}
📝要点总结:
- 闭包的定义如下:
|param1, param2, ...| { block }
,只有一条语句的时候大括号可以省略,参数的类型提示也可以省略。 - 可以看到我们定义了一个闭包:
|| println!("
color: {}", color);
,并把它绑定到print
变量,然后就可以像函数调用那样使用闭包。 - 值得注意的是闭包使用了外部变量
color
,而这个变量并不是它的参数,这就是所谓的闭包可以捕获环境变量。
闭包主要的应用领域是函数式编程,结合迭代器可以清晰的表达一些计算过程:
fn main() {
let upper = 1000;
let sum_of_squared_odd_numbers: u32 = (0..)
.map(|n| n * n)
.take_while(|&n_squared| n_squared < upper)
.filter(|&n_squared| n_squared % 2 == 1)
.fold(0, |acc, n_squared| acc + n_squared);
println!("functional style: {}", sum_of_squared_odd_numbers);
}
上述计算过程一环扣一环,就像流水线一样。以上例子表明:有些函数的参数并不是数字这样的值,而是一种方法策略,这里涉及一个思维的转变:把计算过程当做数据,这样就可以把函数当做某些函数参数与返回值,自然而然就产生了一个概念:高阶函数。所谓的高阶函数就是以其他函数为参数,或者返回函数的函数。
💡 有一类函数特别有意思:它以一些简单的函数为参数,然后返回一个复杂的函数,称为组合子(Combinators),这是一种程序设计风格,Rust 语言有着广泛的应用。
更多资料:https://learning-rust.github.io/docs/e6.combinators.html。
ℹ️ 有关迭代器我们留在后面学习。
Rust 闭包捕获值的方式
❗ 阅读以下内容前请学习 Rust 所有权机制。
Rust 有三种捕获值的方式:
Fn
: 捕获引用 (&T
)FnMut
: 捕获可变引用 (&mut T
)FnOnce
: 捕获值 (T
)
为了揭示他们的区别,先看如下示例:
fn main() {
let hello_str = String::from("Hello");
let f = || drop(hello_str);
f();
f();
}
编译结果:
|
12 | f();
| --- `f` moved due to this call
13 | f();
| ^ value used here after move
|
note: closure cannot be invoked more than once because it moves the variable `hello_str` out of its environment
--> src\main.rs:10:21
|
10 | let f = || drop(hello_str);
| ^^^^^^^^^
note: this value implements `FnOnce`, which causes it to be moved when called
--> src\main.rs:12:5
|
12 | f();
| ^
提示信息的主要内容是:f()
调用把 hello_str
从环境移除了,所以只能调用一次,它实现的是 FnOnce trait
。
当然代码的逻辑是尝试释放 hello_str
两次,这是经典的重复释放(double free)错误,Rust 语言为我们避免了这个错误。
再来看一个例子:
fn call_twice<F>(closure: F)
where
F: Fn(),
{
closure();
closure();
}
fn main() {
let hello_str = String::from("Hello");
let f = || drop(hello_str);
call_twice(f);
}
编译结果:
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
--> src\main.rs:10:13
|
10 | let f = || drop(hello_str);
| ^^^^^^^^---------^
| | |
| | closure is `FnOnce` because it moves the variable `hello_str` out of its environment
| this closure implements `FnOnce`, not `Fn`
11 |
12 | call_twice(f);
| ---------- the requirement to implement `Fn` derives from here
💡 我们可以把 FnOnce
形象的称为杀值的闭包。你可以尝试把 drop(hello_str);
换成let another = hello_str;
他们的效果类似。
再看一个例子:
fn main() {
let name = String::from("Alice");
let hello_from_thread = || format!("Hello: {}", name);
let handler = std::thread::spawn(hello_from_thread);
println!("{:?}", handler.join().unwrap());
}
这个例子的逻辑非常简单,就是调用多线程处理一些工作,编译结果:
error[E0373]: closure may outlive the current function, but it borrows `name`, which is owned by the current function
--> src\main.rs:3:29
|
3 | let hello_from_thread = || format!("Hello: {}", name);
| ^^ ---- `name` is borrowed here
| |
| may outlive borrowed value `name`
|
note: function requires argument type to outlive `'static`
--> src\main.rs:4:19
|
4 | let handler = std::thread::spawn(hello_from_thread);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `name` (and any other referenced variables), use the `move` keyword
|
3 | let hello_from_thread = move || format!("Hello: {}", name);
| ++++
仔细分析一下代码:闭包需要环境变量 name
,然而,在另一个线程运行的时候,无法保证该环境能获取到 name
(outlive),解决方法是用 move
关键字告诉闭包把值整个拿走,而不是借用。
再给一个类似的例子:
fn main() {
let closure = get_print_closure();
closure();
}
fn get_print_closure() -> impl Fn() {
let name = String::from("User");
move || {
println!("Hello {}!", name);
}
}
接下来讲解两个两种借用值的闭包:
fn main() {
let name = String::from("Rust");
let say_hello = || format!("Hello, {}", name);
println!("{:?}", say_hello()); // "Hello, Rust"
let mut plain_rust = String::from("Rust");
let mut awesome_rust = || plain_rust.push_str(" is awesome");
awesome_rust();
println!("{:?}", plain_rust) // "Rust is awesome"
}
这是闭包最常见的用法,分别体现了 Fn, FnMut
特性。
更多资料请阅读:https://zhauniarovich.com/post/2020/2020-12-closures-in-rust/。
本作品采用《CC 协议》,转载必须注明作者和本文链接