变量和可变性
ch03-01-variables-and-mutability.md
commit 04aa3a45eb72855b34213703718f50a12a3eeec8
第二章中提到过,变量默认是不可变(immutable)的。这是利用 Rust 安全和简单并发的优势编写代码一大助力。不过,变量仍然有可变的选项。让我们探讨一下,拥抱不可变性的原因及方法,以及何时你不想拥抱。
当变量不可变时,意味着一旦值被绑定上一个名称,你就不能改变这个值。作为说明,通过cargo new --bin variables
在 projects 目录生成一个叫做 variables 的新项目。
接着,在新建的 variables 目录,打开 src/main.rs 并替换其代码为如下:
Filename: 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.0.1 (file:///projects/variables)
error[E0384]: re-assignment of 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;
| ^^^^^ re-assignment of immutable variable
这个例子展示了编译器如何帮助你找出程序中的错误。即便编译错误令人沮丧,那也不过是说程序不能安全的完成你想让它完成的工作;而不能说明你是不是一个好程序员!有经验的 Rustacean 们一样会遇到编译错误。这些错误给出的原因是对不可变变量重新赋值
(re-assignment of immutable variable
),因为我们尝试对不可变变量x
赋第二个值。
尝试去改变预设为不可变的值,产生编译错误是很重要的,因为这种情况可能导致 bug:如果代码的一部分假设一个值永远也不会改变,而另一部分代码改变了它,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 难以跟踪,尤其是第二部分代码只是有时改变其值。
Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要厘清如何以及哪里可能会被改变,从而使得代码易于推导。
不过可变性也是非常有用的。变量只是默认不可变;可以通过在变量名之前加 mut
来使其可变。它向读者表明了其他代码将会改变这个变量的意图。
例如,改变 src/main.rs 并替换其代码为如下:
Filename: 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)
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
通过 mut
,允许把绑定到 x
的值从 5
改成 6
。在一些情况下,你会想要一个变量可变,因为相对不可变的风格更容易写。
除了避免 bug 外,还有多处需要权衡取舍。例如,使用大型数据结构时,适当地使变量可变,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的风格编程,可能会使代码更易理解,为可读性而遭受性能惩罚或许值得。
变量和常量的区别
不允许改变值的变量,可能会使你想起另一个大部分编程语言都有的概念:常量(constants)。类似于不可变变量,常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。
首先,不允许对常量使用 mut
:常量不光默认不能变,它总是不能变。
声明常量使用 const
关键字而不是 let
,而且必须注明值的类型。在下一部分,“数据类型”,涉及到类型和类型注解,现在无需关心这些细节,记住总是标注类型即可。
常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。
最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果,或任何其他只在运行时计算的值。
这是一个常量声明的例子,它的名称是 MAX_POINTS
,值是 100,000。(常量使用下划线分隔的大写字母命名):
const MAX_POINTS: u32 = 100_000;
常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以作为多处代码使用的全局范围的值,例如一个游戏中所有玩家可以获取的最高分或者光速。
将作用于整个程序的值,由硬编码改为常量(并编写文档),对后来的维护者了解值的意义很用帮助。它也能将硬编码的值汇总一处,为将来可能的修改提供方便。
遮盖(Shadowing)
如第二章“猜猜看游戏”所讲的,我们可以定义一个与之前变量重名的新变量,而新变量会遮盖之前的变量。Rustacean 称之为“第一个变量被第二个遮盖了”,这意味着使用这个变量时会看第二个值。可以用相同变量名称来遮盖它自己,以及重复使用 let
关键字来多次遮盖,如下所示:
Filename: src/main.rs
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("The value of x is: {}", x);
}
这个程序首先将 x
绑定到值 5
上。接着通过 let x =
遮盖 x
,获取原始值并加 1
这样 x
的值就变成 6
了。第三个 let
语句也覆盖了 x
,获取之前的值并乘以 2
,x
最终的值是 12
。运行这个程序,它会有如下输出:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Running `target/debug/variables`
The value of x is: 12
这与将变量声明为 mut
是有区别的。因为除非再次使用 let
关键字,不小心尝试对变量重新赋值会导致编译时错误。我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。
mut
与遮盖的另一个区别是,当再次使用 let
时,实际上创建了一个新变量,我们可以改变值的类型。例如,假设程序请求用户输入空格来提供文本间隔,然而我们真正需要的是将输入存储成数字(多少个空格):
let spaces = " ";
let spaces = spaces.len();
这里允许第一个 spaces
变量是字符串类型,而第二个 spaces
变量,它是一个恰巧与第一个变量同名的崭新变量,是数字类型。遮盖使我们不必使用不同的名字,如 spaces_str
和 spaces_num
;相反,我们可以复用 spaces
这个更简单的名字。然而,如果尝试使用mut
,如下所示:
let mut spaces = " ";
spaces = spaces.len();
会导致一个编译错误,因为改变一个变量的类型是不被允许的:
error[E0308]: mismatched types
--> src/main.rs:3:14
|
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected &str, found usize
|
= note: expected type `&str`
= note: found type `usize`
现在我们探索了变量如何工作,让我们看看更多的数据类型。