在 LabEx 中定义 Rust 函数

RustRustBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

欢迎来到函数。本实验是《Rust 程序设计语言》的一部分。你可以在 LabEx 中练习 Rust 技能。

在本实验中,你将学习如何使用 fn 关键字和传统的蛇形命名法在 Rust 中定义和调用函数。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/MemorySafetyandManagementGroup(["Memory Safety and Management"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/shadowing("Variable Shadowing") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/MemorySafetyandManagementGroup -.-> rust/lifetime_specifiers("Lifetime Specifiers") subgraph Lab Skills rust/variable_declarations -.-> lab-100389{{"在 LabEx 中定义 Rust 函数"}} rust/shadowing -.-> lab-100389{{"在 LabEx 中定义 Rust 函数"}} rust/integer_types -.-> lab-100389{{"在 LabEx 中定义 Rust 函数"}} rust/function_syntax -.-> lab-100389{{"在 LabEx 中定义 Rust 函数"}} rust/expressions_statements -.-> lab-100389{{"在 LabEx 中定义 Rust 函数"}} rust/lifetime_specifiers -.-> lab-100389{{"在 LabEx 中定义 Rust 函数"}} end

函数

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

创建一个名为 functions 的新项目:

cargo new functions
cd functions

Rust 代码使用蛇形命名法作为函数和变量名的传统风格,即所有字母均为小写,单词之间以下划线分隔。下面是一个包含示例函数定义的程序:

文件名:src/main.rs

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

    another_function();
}

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

我们通过输入 fn 后跟函数名和一组括号来在 Rust 中定义函数。花括号告诉编译器函数体从哪里开始和结束。

我们可以通过输入函数名后跟一组括号来调用我们定义的任何函数。因为 another_function 在程序中定义,所以可以在 main 函数内部调用它。请注意,我们在源代码中 main 函数之后定义了 another_function;我们也可以在之前定义它。Rust 并不关心你在哪里定义函数,只关心它们在调用者可见的某个作用域中被定义。

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

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [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 [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

another_function 的声明有一个名为 x 的参数。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 类型。然后该函数会打印包含 valueunit_label 的文本。

让我们尝试运行这段代码。用前面的例子替换你 functions 项目的 src/main.rs 文件中当前的程序,然后使用 cargo run 运行它:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [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; 就是一个语句。

文件名: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 statement (`let`)
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: variable declaration using `let` is a statement

error[E0658]: `let` expressions in this position are unstable
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for
more information

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() {
  1 let y = {2
        let x = 3;
      3 x + 1
    };

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

表达式 [2] 是一个块,在这种情况下,它计算结果为 4。作为 let 语句 [1] 的一部分,那个值被绑定到 y 上。注意最后一行没有分号 [3],这与你目前看到的大多数行不同。表达式不包括结尾的分号。如果你在一个表达式的末尾添加一个分号,你就把它变成了一个语句,然后它将不会返回值。在你接下来探索函数返回值和表达式时,请记住这一点。

有返回值的函数

函数可以向调用它们的代码返回值。我们不给返回值命名,但必须在箭头(->)之后声明它们的类型。在 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 [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,所以这一行与以下内容相同:

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

主要的错误信息 “mismatched types” 揭示了这段代码的核心问题。函数 plus_one 的定义表明它将返回一个 i32,但语句不会计算出一个值,这由单元类型 () 表示。因此,没有返回任何值,这与函数定义相矛盾并导致错误。在这个输出中,Rust 提供了一条可能有助于纠正此问题的消息:它建议删除分号,这将修复错误。

总结

恭喜你!你已经完成了函数实验。你可以在 LabEx 中练习更多实验来提升你的技能。