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 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!