变量和可变性

ch03-01-variables-and-mutability.md
commit d69b1058c660abfe1d274c58d39c06ebd5c96c47

第二章中提到过,变量默认是不可改变的(immutable)。这是推动你以充分利用 Rust 提供的安全性和简单并发性来编写代码的众多方式之一。不过,你仍然可以使用可变变量。让我们探讨一下 Rust 为何及如何鼓励你利用不可变性,以及何时你会选择不使用不可变性。

当变量不可变时,一旦值被绑定一个名称上,你就不能改变这个值。为了对此进行说明,使用 cargo new variables 命令在 projects 目录生成一个叫做 variables 的新项目。

接着,在新建的 variables 目录,打开 src/main.rs 并将代码替换为如下代码,这些代码还不能编译:

文件名: src/main.rs

  1. fn main() {
  2. let x = 5;
  3. println!("The value of x is: {}", x);
  4. x = 6;
  5. println!("The value of x is: {}", x);
  6. }

保存并使用 cargo run 运行程序。应该会看到一条错误信息,如下输出所示:

  1. error[E0384]: cannot assign twice to immutable variable `x`
  2. --> src/main.rs:4:5
  3. |
  4. 2 | let x = 5;
  5. | - first assignment to `x`
  6. 3 | println!("The value of x is: {}", x);
  7. 4 | x = 6;
  8. | ^^^^^ cannot assign twice to immutable variable

这个例子展示了编译器如何帮助你找出程序中的错误。虽然编译错误令人沮丧,但那只是表示程序不能安全的完成你想让它完成的工作;并 不能 说明你不是一个好程序员!经验丰富的 Rustacean 们一样会遇到编译错误。

错误信息指出错误的原因是 不能对不可变变量 x 二次赋值cannot assign twice to immutable variable x),因为你尝试对不可变变量 x 赋第二个值。

在尝试改变预设为不可变的值时,产生编译时错误是很重要的,因为这种情况可能导致 bug。如果一部分代码假设一个值永远也不会改变,而另一部分代码改变了这个值,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 的起因难以跟踪,尤其是第二部分代码只是 有时 会改变值。

Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要追踪一个值如何和在哪可能会被改变,从而使得代码易于推导。

不过可变性也是非常有用的。变量只是默认不可变;正如在第二章所做的那样,你可以在变量名之前加 mut 来使其可变。除了允许改变值之外,mut 向读者表明了其他代码将会改变这个变量值的意图。

例如,让我们将 src/main.rs 修改为如下代码:

文件名: src/main.rs

  1. fn main() {
  2. let mut x = 5;
  3. println!("The value of x is: {}", x);
  4. x = 6;
  5. println!("The value of x is: {}", x);
  6. }

现在运行这个程序,出现如下内容:

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

通过 mut,允许把绑定到 x 的值从 5 改成 6。在一些情况下,你会想用可变变量,因为与只用不可变变量相比,它会让代码更容易编写。

除了防止出现 bug 外,还有很多地方需要权衡取舍。例如,使用大型数据结构时,适当地使用可变变量,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的编程风格,可能会使代码更易理解,为可读性而牺牲性能或许是值得的。

变量和常量的区别

不允许改变值的变量,可能会使你想起另一个大部分编程语言都有的概念:常量constants)。类似于不可变变量,常量是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。

首先,不允许对常量使用 mut。常量不光默认不能变,它总是不能变。

声明常量使用 const 关键字而不是 let,并且 必须 注明值的类型。在下一部分,“数据类型” 中会介绍类型和类型注解,现在无需关心这些细节,记住总是标注类型即可。

常量可以在任何作用域中声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。

最后一个区别是,常量只能被设置为常量表达式,而不能是函数调用的结果,或任何其他只能在运行时计算出的值。

这是一个声明常量的例子,它的名称是 MAX_POINTS,值是 100,000。(Rust 常量的命名规范是使用下划线分隔的大写字母单词,并且可以在数字字面值中插入下划线来提升可读性):

  1. const MAX_POINTS: u32 = 100_000;

在声明它的作用域之中,常量在整个程序生命周期中都有效,这使得常量可以作为多处代码使用的全局范围的值,例如一个游戏中所有玩家可以获取的最高分或者光速。

将遍布于应用程序中的硬编码值声明为常量,能帮助后来的代码维护人员了解值的意图。如果将来需要修改硬编码值,也只需修改汇聚于一处的硬编码值。

隐藏(Shadowing)

正如在第二章猜猜看游戏的 “比较猜测的数字和秘密数字” 中所讲,我们可以定义一个与之前变量同名的新变量,而新变量会 隐藏 之前的变量。Rustacean 们称之为第一个变量被第二个 隐藏 了,这意味着使用这个变量时会看到第二个值。可以用相同变量名称来隐藏一个变量,以及重复使用 let 关键字来多次隐藏,如下所示:

文件名: src/main.rs

  1. fn main() {
  2. let x = 5;
  3. let x = x + 1;
  4. let x = x * 2;
  5. println!("The value of x is: {}", x);
  6. }

这个程序首先将 x 绑定到值 5 上。接着通过 let x = 隐藏 x,获取初始值并加 1,这样 x 的值就变成 6 了。第三个 let 语句也隐藏了 x,将之前的值乘以 2x 最终的值是 12。运行这个程序,它会有如下输出:

  1. $ cargo run
  2. Compiling variables v0.1.0 (file:///projects/variables)
  3. Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
  4. Running `target/debug/variables`
  5. The value of x is: 12

隐藏与将变量标记为 mut 是有区别的。当不小心尝试对变量重新赋值时,如果没有使用 let 关键字,就会导致编译时错误。通过使用 let,我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。

mut 与隐藏的另一个区别是,当再次使用 let 时,实际上创建了一个新变量,我们可以改变值的类型,但复用这个名字。例如,假设程序请求用户输入空格字符来说明希望在文本之间显示多少个空格,然而我们真正需要的是将输入存储成数字(多少个空格):

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

这里允许第一个 spaces 变量是字符串类型,而第二个 spaces 变量,它是一个恰巧与第一个变量同名的崭新变量,是数字类型。隐藏使我们不必使用不同的名字,如 spaces_strspaces_num;相反,我们可以复用 spaces 这个更简单的名字。然而,如果尝试使用 mut,将会得到一个编译时错误,如下所示:

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

这个错误说明,我们不能改变变量的类型:

  1. error[E0308]: mismatched types
  2. --> src/main.rs:3:14
  3. |
  4. 3 | spaces = spaces.len();
  5. | ^^^^^^^^^^^^ expected &str, found usize
  6. |
  7. = note: expected type `&str`
  8. found type `usize`

现在我们已经了解了变量如何工作,让我们看看变量可以拥有的更多数据类型。