函数

函数在 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 函数内部调用它。请注意,我们在源代码中 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 的参数。 x 的类型被指定为 i32。当我们传递 5another_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。然后该函数打印包含 valueunit_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

因为我们将函数调用时使用 5 作为 value 的值,使用 'h' 作为 unit_label 的值,所以程序输出包含这些值。

语句和表达式

函数体由一系列语句组成,可选地以一个表达式结束。到目前为止,我们讨论的函数还没有包含结束表达式,但你已经看到了作为语句一部分的表达式。因为 Rust 是一种基于表达式的语言,理解这一点非常重要。其他语言没有相同的区别,所以让我们看看语句和表达式是什么,以及它们的区别如何影响函数体。

  • 语句是执行某些操作的指令,不会返回值。
  • 表达式 会计算出一个结果值。让我们来看一些例子。

我们实际上已经使用过语句和表达式。使用 let 关键字创建变量并赋值是一个语句。在示例 3-1 中,let y = 6; 是一个语句。

Filename: src/main.rs
fn main() {
    let y = 6;
}
Listing 3-1: A main function declaration containing one statement

函数定义也是语句;前面的整个例子本身就是一个语句。(正如我们将在下面看到的,调用函数不是语句。)

语句不返回值。因此,你不能将一个 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)]` 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 并且 xy 都会有值 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

5five 中是函数的返回值,这就是为什么返回类型是 i32。让我们更详细地 examine this。有两个重要的部分:首先,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 提供了一条消息,可能有助于解决这个问题:它建议删除分号,这将修复错误。