变量与可变性
如“使用变量存储值”一节所述,变量默认不可变。这是 Rust 鼓励你编写安全代码、充分利用其并发优势的方式之一。不过,你仍然可以选择让变量可变。让我们看看 Rust 为什么鼓励优先使用不可变变量,以及在什么情况下你可能会选择不这样做。
变量不可变时,一旦某个值绑定到变量名,就不能再改变该值。为了说明这一点,请在 projects 目录中使用 cargo new variables 创建名为 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`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
这个例子展示了编译器如何帮助你发现程序中的错误。编译器错误可能令人沮丧,但它们只说明程序还没有安全地实现你的预期;并不意味着你不是一个好程序员!经验丰富的 Rustacean 也会遇到编译器错误。
你收到错误消息 cannot assign twice to immutable variable `x`,是因为你试图给不可变变量 x 第二次赋值。
试图修改声明为不可变的值时,编译器能及时报错非常重要,因为这种情况可能导致 bug。如果代码的一部分假设某个值永远不会改变,而另一部分代码却修改了该值,那么前一部分代码可能无法按设计工作。这类 bug 事后很难追查,尤其是当后一部分代码有时才会改变该值时。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` profile [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,并且必须标注值的类型。下一节“数据类型”会介绍类型和类型标注,现在不必担心细节,只要记住必须始终标注类型。
常量可以在任何作用域中声明,包括全局作用域,因此适合存放代码中许多部分都需要了解的值。
最后一个区别是,常量只能被设置为常量表达式,而不能是只能在运行时计算出来的值。
下面是常量声明的示例:
#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}
这个常量名为 THREE_HOURS_IN_SECONDS,其值是 60(一分钟的秒数)乘以 60(一小时的分钟数)再乘以 3(本程序要计算的小时数)的结果。Rust 的常量命名惯例是全部使用大写字母,并用下划线分隔单词。编译器可以在编译时计算有限种操作,因此我们可以通过这种更易于理解和验证的方式写出这个值,而不是直接将常量设为 10,800。有关声明常量时可用操作的更多信息,请参阅 Rust 参考中的常量求值一节。
在声明它们的作用域内,常量在程序运行期间始终有效。因此,常量适合存放应用领域中程序多个部分都可能需要了解的值,例如游戏中玩家可获得的最高分,或光速。
将程序中重复使用的硬编码值命名为常量,有助于向未来维护代码的人表达这些值的含义。如果将来需要更新硬编码值,也只需在代码中的一个位置修改。
遮蔽
如你在第 2 章的猜数字教程中所见,可以声明一个与先前变量同名的新变量。Rustacean 称第一个变量被第二个变量遮蔽,也就是说,使用该变量名时,编译器看到的是第二个变量。实际上,第二个变量会覆盖第一个变量名的作用,直到它自己也被遮蔽或当前作用域结束。我们可以像下面这样重复使用同一个变量名和 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` profile [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 关键字实际上创建了一个新变量,因此我们可以改变值的类型,同时重复使用同一个名称。例如,假设程序要求用户输入空格字符来表示文本之间需要多少个空格,然后我们想将输入保存为数字:
fn main() {
let spaces = " ";
let spaces = spaces.len();
}
第一个 spaces 变量是字符串类型,第二个 spaces 变量则是数字类型。借助遮蔽,我们不必想出 spaces_str 和 spaces_num 这样的不同名称,而是可以重复使用更简单的 spaces。不过,如果像下面这样尝试使用 mut,就会在编译时遇到错误:
fn main() {
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`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error
了解变量的工作方式后,接下来看看它们还可以使用哪些数据类型。