Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

函数

函数在 Rust 代码中十分常见。你已经见过语言中最重要的函数之一:main 函数,它是许多程序的入口点。你也见过用于声明新函数的 fn 关键字。

Rust 代码中的函数名和变量名通常采用蛇形命名法:所有字母均为小写,单词之间用下划线分隔。下面的程序展示了一个函数定义:

文件名:src/main.rs

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

在 Rust 中,输入 fn、函数名和一对圆括号即可定义函数。花括号告诉编译器函数体从哪里开始、在哪里结束。

要调用已经定义的函数,只需输入函数名和一对圆括号。由于程序中定义了 another_function,因此可以在 main 函数中调用它。注意,源代码中 another_function 定义在 main 函数之后;当然也可以定义在之前。Rust 不在意函数定义的位置,只要求函数定义在调用方可见的某个作用域中。

让我们创建一个名为 functions 的二进制项目,进一步探索函数。将 another_function 示例放入 src/main.rs 并运行。你应该会看到以下输出:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/functions`
Hello, world!
Another function.

代码会按照它们在 main 函数中出现的顺序执行。首先打印“Hello, world!”,然后调用 another_function 并打印它的消息。

参数

我们可以为函数定义参数,它们是函数签名中的特殊变量。函数有参数时,可以为这些参数提供具体的值。严格来说,这些具体值称为实参;不过在日常交流中,人们常常会混用参数和实参,既指函数定义中的变量,也指调用函数时传入的具体值。

下面这个版本的 another_function 添加了一个参数:

文件名:src/main.rs

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

试着运行这个程序,你应该会得到以下输出:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

another_function 的声明包含一个名为 x 的参数,其类型指定为 i32。将 5 传给 another_function 时,println! 宏会把 5 放到格式字符串中包含 x 的那对花括号所在的位置。

在函数签名中,必须声明每个参数的类型。这是 Rust 设计中的一项有意选择:要求在函数定义中标注类型,意味着编译器几乎不需要你在代码的其他地方再标注类型,就能推断出你的意图。如果编译器知道函数期望的类型,也能给出更有帮助的错误消息。

定义多个参数时,用逗号分隔各参数声明,如下所示:

文件名:src/main.rs

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

这个例子创建了一个名为 print_labeled_measurement、带有两个参数的函数。第一个参数名为 value,类型为 i32;第二个参数名为 unit_label,类型为 char。然后,函数会打印一段同时包含 value 和 unit_label 的文本。

试着运行这段代码。将 functions 项目的 src/main.rs 中现有程序替换为上述示例,然后使用 cargo run 运行:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/functions`
The measurement is: 5h

调用函数时,我们为 value 传入了 5,为 unit_label 传入了 'h',因此程序输出中会包含这两个值。

语句与表达式

函数体由一系列语句组成,并可以选择以表达式结尾。目前介绍的函数都没有以表达式结尾,但你已经见过语句中的表达式。由于 Rust 是一种基于表达式的语言,理解这个区别非常重要。其他语言不一定有相同的区分,让我们来看看语句和表达式是什么,以及它们的差异如何影响函数体。

  • 语句是执行某些操作但不返回值的指令。
  • 表达式会计算并产生一个值。

来看几个例子。

实际上,我们已经使用过语句和表达式。使用 let 关键字创建变量并为其赋值,就是一条语句。在代码清单 3-1 中,let y = 6; 是一条语句。

文件名:src/main.rs
fn main() {
    let y = 6;
}
代码清单 3-1:包含一条语句的 main 函数声明

函数定义也是语句;前面的整个例子本身就是一条语句。(不过我们很快会看到,调用函数并不是语句。)

语句不会返回值。因此,你不能像下面这样将 let 语句赋值给另一个变量,否则就会出错:

文件名:src/main.rs

fn main() {
    let x = (let y = 6);
}

运行这个程序时,会得到如下错误:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^
  |
  = note: only supported directly in conditions of `if` and `while` expressions

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^         ^
  |
  = note: `#[warn(unused_parens)]` (part of `#[warn(unused)]`) on by default
help: remove these parentheses
  |
2 -     let x = (let y = 6);
2 +     let x = let y = 6 ;
  |

warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted

let y = 6 语句不会返回值,因此没有任何值可以绑定到 x。这与 C 和 Ruby 等其他语言不同,在那些语言中,赋值会返回所赋的值。你可以写出 x = y = 6,让 x 和 y 都等于 6;但 Rust 并非如此。

表达式会计算出一个值,并构成你在 Rust 中编写的大部分其他代码。例如,数学运算 5 + 6 是一个表达式,计算结果为 11。表达式可以是语句的一部分:在代码清单 3-1 中,语句 let y = 6; 里的 6 是一个计算结果为 6 的表达式。调用函数是表达式,调用宏也是表达式。花括号创建的新作用域块同样是表达式,例如:

文件名:src/main.rs

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {y}");
}

这个表达式:

{
    let x = 3;
    x + 1
}

是一个代码块,在本例中会计算出 4。作为 let 语句的一部分,该值会绑定到 y。注意,x + 1 这一行末尾没有分号,这与目前看到的大多数代码行不同。表达式末尾不带分号。如果在表达式末尾添加分号,它就会变成语句,也就不会返回值。接下来了解函数返回值和表达式时,请记住这一点。

带返回值的函数

函数可以向调用它的代码返回值。我们不为返回值命名,但必须在箭头(->)后声明其类型。在 Rust 中,函数的返回值就是函数体代码块中最后一个表达式的值。你可以使用 return 关键字并指定一个值,让函数提前返回;不过,大多数函数都会隐式返回最后一个表达式。下面是一个返回值的函数示例:

文件名:src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {x}");
}

five 函数中没有函数调用、宏,甚至没有 let 语句,只有数字 5。这在 Rust 中完全是有效的函数。注意,函数的返回类型也已指定为 -> i32。试着运行这段代码,输出应该如下所示:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
The value of x is: 5

five 中的 5 是函数的返回值,因此返回类型为 i32。让我们更详细地看看这里的要点。首先,let x = five(); 这一行表明,我们使用函数的返回值来初始化变量。由于 five 函数返回 5,这一行就等同于:

#![allow(unused)]
fn main() {
let x = 5;
}

其次,five 函数没有参数,并声明了返回值类型;函数体中只有一个孤零零的 5,末尾没有分号,因为它是我们希望返回的表达式。

再来看一个例子:

文件名:src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

运行这段代码会打印 The value of x is: 6。但是,如果在 x + 1 这一行末尾加上分号,让它从表达式变为语句,会发生什么呢?

文件名:src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1;
}

编译这段代码会产生以下错误:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: remove this semicolon to return this value

For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error

主要错误消息 mismatched types 揭示了这段代码的核心问题。plus_one 函数的定义表明它会返回 i32,但语句不会计算出值;语句对应的值是单元类型 ()。因此函数实际上没有返回任何值,这与函数定义相矛盾,导致了错误。在这段输出中,Rust 还提供了可能有助于修正问题的提示:建议删除分号即可解决错误。