变量与可变性

RustRustBeginner
立即练习

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

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

简介

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

在本实验中,我们将探讨 Rust 中的可变性概念,讨论变量默认情况下是不可变的,以及如何使用 mut 关键字使其可变,并强调不可变性对于安全性和并发性的重要性,同时也承认可变性在某些情况下的有用性。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/BasicConceptsGroup -.-> rust/constants_usage("Constants Usage") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100387{{"变量与可变性"}} rust/mutable_variables -.-> lab-100387{{"变量与可变性"}} rust/constants_usage -.-> lab-100387{{"变量与可变性"}} rust/integer_types -.-> lab-100387{{"变量与可变性"}} rust/function_syntax -.-> lab-100387{{"变量与可变性"}} rust/expressions_statements -.-> lab-100387{{"变量与可变性"}} rust/method_syntax -.-> lab-100387{{"变量与可变性"}} end

变量与可变性

正如在“使用变量存储值”中提到的,默认情况下,变量是不可变的。这是 Rust 引导你以利用其提供的安全性和易于并发的方式编写代码的众多方式之一。不过,你仍然可以选择使变量可变。让我们来探讨一下 Rust 如何以及为何鼓励你倾向于使用不可变性,以及为何有时你可能想要选择可变。

当一个变量是不可变的时,一旦一个值绑定到一个名称,你就不能更改该值。为了说明这一点,通过使用 cargo new variables 在你的 project 目录中生成一个名为 _variables_ 的新项目。

然后,在你新的 variables 目录中,打开 src/main.rs 并将其代码替换为以下代码,这段代码目前还无法编译:

文件名:src/main.rs

fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

保存并使用 cargo run 运行该程序。你应该会收到一条关于不可变性错误的错误消息,如下所示:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     println!("The value of x is: {x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

这个例子展示了编译器如何帮助你在程序中发现错误。编译器错误可能会令人沮丧,但实际上它们仅仅意味着你的程序尚未安全地完成你想要它做的事情;它们并不意味着你不是一个好程序员!经验丰富的 Rust 开发者仍然会遇到编译器错误。

你收到错误消息“不能对不可变变量 x 进行两次赋值”,是因为你试图对不可变的 x 变量赋第二个值。

当我们尝试更改被指定为不可变的值时会得到编译时错误,这一点很重要,因为这种情况可能会导致错误。如果我们代码的一部分基于某个值永远不会改变的假设进行操作,而另一部分代码更改了该值,那么代码的第一部分可能无法按预期运行。这种错误的原因在事后可能很难追查,尤其是当第二部分代码只是有时更改该值时。Rust 编译器保证当你声明一个值不会改变时,它真的不会改变,所以你不必自己去跟踪它。因此,你的代码更易于理解。

但是可变性可能非常有用,并且可以使代码编写起来更方便。虽然变量默认是不可变的,但你可以像在第 2 章中那样,通过在变量名前添加 mut 使其可变。添加 mut 还通过表明代码的其他部分将更改此变量的值,向代码的未来读者传达了意图。

例如,让我们将 src/main.rs 更改为以下内容:

文件名:src/main.rs

fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

现在当我们运行程序时,会得到如下结果:

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

当使用 mut 时,我们可以将绑定到 x 的值从 5 更改为 6。最终,决定是否使用可变性取决于你,并且取决于你认为在特定情况下哪种方式最清晰。

常量

与不可变变量类似,常量 是绑定到一个名称且不允许更改的值,但常量和变量之间存在一些差异。

首先,不允许对常量使用 mut。常量不仅默认是不可变的 —— 它们始终是不可变的。你使用 const 关键字而不是 let 关键字来声明常量,并且必须注释值的类型。我们将在“数据类型”中介绍类型和类型注释,所以现在不用担心细节。只需知道你必须始终注释类型。

常量可以在任何作用域中声明,包括全局作用域,这使得它们对于代码的许多部分都需要了解的值很有用。

最后一个区别是,常量只能设置为常量表达式,而不能设置为只能在运行时计算出的值。

下面是一个常量声明的示例:

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

常量的名称是 THREE_HOURS_IN_SECONDS,其值设置为 60(一分钟中的秒数)乘以 60(一小时中的分钟数)乘以 3(我们在这个程序中要计算的小时数)的结果。Rust 中常量的命名规范是使用全大写字母,单词之间用下划线分隔。编译器能够在编译时计算一组有限的操作,这使我们可以选择以更易于理解和验证的方式写出这个值,而不是将这个常量设置为 10800。有关声明常量时可以使用哪些操作的更多信息,请参阅 Rust 参考文档中关于常量求值的部分 https://doc.rust-lang.org/reference/const_eval.html

在声明常量的作用域内,常量在程序运行的整个期间都是有效的。这个属性使得常量对于程序的多个部分可能需要了解的应用领域中的值很有用,例如游戏中任何玩家允许获得的最大分数,或者光速。

将程序中使用的硬编码值命名为常量,有助于向代码的未来维护者传达该值的含义。如果将来需要更新硬编码值,这也有助于在代码中只需要在一个地方进行更改。

遮蔽

正如你在第 2 章的猜数字游戏教程中看到的,你可以声明一个与先前变量同名的新变量。Rust 开发者说第一个变量被第二个变量遮蔽了,这意味着当你使用变量名时,编译器看到的是第二个变量。实际上,第二个变量遮蔽了第一个变量,将变量名的任何使用都归为自身,直到它自己被遮蔽或作用域结束。我们可以通过使用相同的变量名并重复使用 let 关键字来遮蔽一个变量,如下所示:

文件名:src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

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

这个程序首先将 x 绑定到值 5。然后它通过重复 let x = 创建一个新变量 x,取原始值并加 1,所以 x 的值变为 6。然后,在由花括号创建的内部作用域中,第三个 let 语句也遮蔽了 x 并创建一个新变量,将前一个值乘以 2 得到 x 的值为 12。当那个作用域结束时,内部遮蔽结束,x 又变回 6。当我们运行这个程序时,它将输出以下内容:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6

遮蔽与将变量标记为 mut 不同,因为如果我们不小心尝试在不使用 let 关键字的情况下重新赋值给这个变量,我们会得到一个编译时错误。通过使用 let,我们可以对一个值进行一些转换,但在转换完成后变量仍然是不可变的。

mut 和遮蔽之间的另一个区别是,因为当我们再次使用 let 关键字时实际上是创建了一个新变量,所以我们可以改变值的类型但重用相同的名称。例如,假设我们的程序要求用户通过输入空格字符来显示他们想要在某些文本之间留多少个空格,然后我们想将该输入存储为一个数字:

let spaces = "   ";
let spaces = spaces.len();

第一个 spaces 变量是字符串类型,第二个 spaces 变量是数字类型。因此,遮蔽使我们不必想出不同的名称,比如 spaces_strspaces_num;相反,我们可以重用更简单的 spaces 名称。然而,如果我们尝试像这样使用 mut,我们会得到一个编译时错误:

let mut spaces = "   ";
spaces = spaces.len();

错误信息表明我们不允许改变变量的类型:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
2 |     let mut spaces = "   ";
  |                      ----- expected due to this value
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

既然我们已经探讨了变量的工作原理,让我们来看看它们可以拥有的更多数据类型。

总结

恭喜你!你已经完成了「变量与可变性」实验。你可以在 LabEx 中练习更多实验来提升你的技能。