变量和可变性

ch03-01-variables-and-mutability.md
commit 04aa3a45eb72855b34213703718f50a12a3eeec8

第二章中提到过,变量默认是不可变immutable)的。这是利用 Rust 安全和简单并发的优势编写代码一大助力。不过,变量仍然有可变的选项。让我们探讨一下,拥抱不可变性的原因及方法,以及何时你不想拥抱。

当变量不可变时,意味着一旦值被绑定上一个名称,你就不能改变这个值。作为说明,通过cargo new --bin variablesprojects 目录生成一个叫做 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,获取之前的值并乘以 2x 最终的值是 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_strspaces_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`

现在我们探索了变量如何工作,让我们看看更多的数据类型。