变量与可变性

Variables and Mutability

如同 “使用变量存储值” 小节所提到的,默认情况下,变量是不可变的。这是 Rust 给到咱们,以充分利用 Rust 所提供的安全性和易并发性的方式,编写咱们代码的措施之一。不过,咱们仍然可以选择让变量可变。我们来探讨一下,Rust 如何以及为什么鼓励咱们偏爱不可变性,以及为什么有时咱们可能会想要选择放弃。

当某个变量是不可变的时,那么一旦某个值绑定到某个名字,咱们就无法更改那个值了。为了说明这一点,请使用 cargo new variables,在 projects 目录中生成一个名为 variables 的新项目。

然后,在咱们的新 variables 目录下,打开 src/main.rs,用下面的代码替换其代码,这段代码现在还不会编译:

fn main() {
    let x = 5;
    println! ("x 的值为:{}", x);

    x = 6;
    println! ("x 的值为:{}", x);
}

保存并使用 cargo run 运行这个程序。咱们应该会收到一条,有关不变性错误的错误信息,如下面这个输出所示:

$ cargo run                                                    ✔
   Compiling variables v0.1.0 (/home/peng/rust-lang/projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:5:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
...
5 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` due to previous error

这个示例展示了,编译器如何帮助咱们发现咱们程序中的错误。编译器报错可能会令人沮丧,但实际上他们只意味着咱们的程序,还不能安全地完成咱们想要他做的事情;他们并 意味着咱们不是一名优秀的程序员!经验丰富的 Rustaceans 仍然会遇到编译器报错。

咱们之所以收到了错误消息 cannot assing twice to immutable variable 'x',是因为咱们试图将第二个值,赋值给那个不可变的变量 x

重要的是,当我们试图更改某个被指定为不可变的值时,会出现编译时错误,因为这种情况可能会导致错误。如果我们代码的一部分假设某个值永远不会改变,而代码的另一部分却改变了该值,那么代码的前一部分就有可能无法完成其设计目标。这种错误的原因可能很难事后追踪,尤其是当第二部分代码只是 偶尔 改变值时。Rust 编译器保证,当咱们声明了某个值不会改变时,他确实不会改变,因此咱们不必自己跟踪它。这样,咱们的代码就更容易推理了。

但可变会非常有用,可以让代码编写更方便。虽然变量在默认情况下是不可变的,但也可以像 第 2 章 中咱们曾做的那样,在变量名前添加 mut,使其成为可变变量。添加 mut 还可以向代码的未来读者,传达某种意图,即代码的其他部分,将改变该变量的值。

比如,咱们来将 src/main.rs 修改为下面这样:

文件名:src/main.rs

fn main() {
    let mut x = 5;
    println! ("x 的值为:{}", x);

    x = 6;
    println! ("x 的值为:{}", x);
}

当我们现在运行这个程序时,会得到这样的结果:

#![allow(unused)]
fn main() {
$ cargo run
   Compiling variables v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\variables)
    Finished dev [unoptimized + debuginfo] target(s) in 1.26s
     Running `target\debug\variables.exe`
x 的值为:5
x 的值为:6
}

在使用了 mut 时,我们就可以将绑定到 x 的值,从 5 改为 6。最终,是否使用可变,取决于咱们自己,取决于咱们认为在特定情况下,什么是最清晰的。

常量

Constants

与不可变的变量一样,常量,constants 也是绑定到名字,且不允许更改的值,但常量和变量之间有些区别。

首先,不允许将 mut 与常量一起使用。常量不仅默认是不可变的,而且始终不可变。咱们要使用 const 关键字,而不是 let 关键字,声明常量,而且 必须 注解值的类型。我们将在下一小节 “数据类型” 中,介绍类型和类型注解,所以现在不用担心这些细节。只需知道咱们必须始终注解类型即可。

常量可以在任何作用域(包括全局作用域)中声明,因此对于代码的多个部分都需要知道的那些值来说,常量非常有用。

常量与不可变变量的最后一个区别,是常量只能设置为一个常量表达式,而不是只能在运行时计算出的值的结果,constants may be set only to a constant expression, not the result of a value that could only be computed at runtime。

译注:这句话的意思,常量的初始化赋值表达式不能包含变量。如下面的代码:

fn main() {
    let a = 3;

    const THREE_HOURS_IN_SECONDS: u32 = a * 60 * 60;

    println! ("三个小时的秒数:{THREE_HOURS_IN_SECONDS}");
}

将报错:

$ cargo run
   Compiling variables v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\variables)
error[E0435]: attempt to use a non-constant value in a constant
 --> src\main.rs:4:41
  |
4 |     const THREE_HOURS_IN_SECONDS: u32 = a * 60 * 60;
  |     ----------------------------        ^ non-constant value
  |     |
  |     help: consider using `let` instead of `const`: `let THREE_HOURS_IN_SECONDS`

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

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

#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}

这个常量的名字是 THREE_HOURS_IN_SECONDS,其值被设置为 60(一分钟的秒数)乘以 60(一小时的分钟数)再乘以 3(本程序中要计算的小时数)的结果。Rust 的常量命名约定,是使用全大写字母,单词之间使用下划线。编译器可以在编译时,计算一个有限的运算集,这让我们可以选择,以一种更容易理解和验证的方式写出这个值,而不是将这个常量设置为值 10,800。有关在声明常量时,可以使用哪些运算的更多信息,请参阅 Rust 参考手册中,关于常量求值的小节

常量在程序运行的整个过程中,在其声明的范围内都有效。这一属性使得常量对于程序域中,多个部分可能需要了解的值,例如游戏中允许任何玩家获得的最大点数或光速,会非常有用。

将整个程序中要用到的一些值,命名为常量,有助于向未来的代码维护者,传达该值的含义。此外,如果将来需要更新硬编码值,只需更改咱们代码中的一处即可。

遮蔽

Shadowing

如同咱们曾在 第 2 章 的猜数游戏教程中,所看到的,咱们可以声明出一个,与先前的某个变量同名的新变量。Rustaceans 会说,第一个变量是被第二个 遮蔽了,shadowd,这意味着当你使用这个变量的名字时,编译器将看到的,是第二个变量。实际上,第二个变量覆盖了第一个,将变量名的任何使用,都带到自己身上,直到他自己被遮蔽,或作用域结束。像下面这样,通过使用相同的变量名,和重复使用 let 关键字,咱们便可以遮蔽某个变量:

文件名:src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println! ("内部作用域中 x 的值为:{}", x);
    }

    println! ("x 的值为:{}", x);
}
内部作用域中 x 的值为:12
x 的值为:6

注意:遮蔽特性的使用,不需要 mut 关键字。

这个程序首先将 x,绑定到 5 的值。然后通过重复 let x =,创建出一个新变量 x,将原来的值加上 1,这样 x 的值就是 6。然后,在用大括号创建的一个内层作用域中,第三个 let 语句也对 x 进行了遮蔽处理,并创建了一个新变量,将先前的值乘以 2,使 x 的值为 12。当这个作用域结束时,内部的遮蔽结束,x 返回到 6。当我们运行这个程序时,他将输出如下内容:

$ cargo run
   Compiling variables v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\variables)
    Finished dev [unoptimized + debuginfo] target(s) in 1.03s
     Running `target\debug\variables.exe`
内部作用域中 x 的值为:12
x 的值为:6

遮蔽操作不同于将变量构造为 mut,因为如果我们不小心在没有使用 let 关键字的情况下,将变量重新赋值,咱们就会得到一个编译时报错。而通过使用 let 关键字,我们可以对某个值执行一些变换,但在这些变换完成后,该变量将不可变。

mut 与遮蔽的另一个区别是,当我们再次使用 let 关键字时,我们实际上是创建了一个新变量,因此我们可以改变该值的类型,而可以重复使用这个同样的名字。例如,假设咱们的程序要求用户,通过输入一些空格字符,来给出他们想要的某些文本之间多少个空格,然后我们打算将这个输入,存储为一个数字:

#![allow(unused)]
fn main() {
let spaces = "    ";
let spaces = spaces.len();
}

第一个 spaces 变量,属于字符串类型,而第二个 spaces 变量,则是数字类型。遮蔽就这样,让我们不必使用不同的名称,如 spaces_strspaces_num;相反,我们可以重复使用这个更简单的 spaces 名称。然而,如果我们尝试使用 mut 来实现这一点,如下所示,咱们就会收到一个编译时报错:

#![allow(unused)]
fn main() {
let mut spaces = "    ";
spaces = spaces.len();
}

该报错表明,我们不允许改变变量的类型:

$ cargo run
   Compiling variables v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\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`
  |
help: try removing the method call
  |
3 -     spaces = spaces.len();
3 +     spaces = spaces;
  |

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

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

Last change: 2023-12-08, commit: 1e2d49a