在上一章,我们成功地运行了第一个 Rust 程序,并提出了一个核心问题:在没有 GC 的情况下,Rust 如何保证内存安全?答案指向了 Rust 的灵魂——“所有权”(Ownership)。
在深入那个庞大而精妙的系统之前,我们必须先掌握一个看似简单,却蕴含着 Rust 核心安全哲学的基础知识:变量的声明与赋值。对于在 Java 世界里畅游多年的我们来说,这似乎是“不值一提”的小事。但相信我,正是从这里开始,你将体会到第一次深刻的思维转变。
Rust 的方式:默认不可变
让我们在 cargo new variables
创建的新项目中的 main.rs
里写下几行非常简单的代码:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6; // 尝试修改 x 的值
println!("The value of x is: {}", x);
}
这段代码的意图非常明显:声明一个变量 x
并赋值为 5,打印它,然后把它修改为 6,再打印出来。在 Java 中,这是天经地义的操作。
然而,当我们运行 cargo run
时,迎接我们的不是成功的结果,而是一条鲜红的编译错误!
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is: {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
请仔细阅读这段错误信息。Rust 编译器不但精确地指出了错误的位置(x = 6;
)和原因(cannot assign twice to immutable variable
),甚至还贴心地给出了修复建议(help: consider making this binding mutable: mut x
)。这就是我们常说的:“Rust 的编译器是你的良师益友”。
这个错误揭示了 Rust 的一个核心原则:变量默认是不可变的(Immutable by default)。
思维转变:从 final
到 let
现在,让我们暂停一下,回到我们熟悉的 Java。在 Java 中,如果你想实现同样的效果——让一个变量不可变——你会怎么做?
你会使用 final
关键字:
final int x = 5;
x = 6; // 这同样会在 Java 中导致编译错误
看到了吗?这正是思维转变的关键点:
- 在 Java 中,变量默认是可变的。 你需要主动选择(Opt-in) 通过
final
关键字来获得不可变性。这是一种值得提倡的“良好实践”,但并非强制。 - 在 Rust 中,变量默认是不可变的。 你需要主动选择(Opt-in) 通过一个关键字来获得可变性。这是一种“默认安全”的语言设计。
Rust 为什么要这么“固执”地选择默认不可变呢?这并非无事生非,而是出于深思熟虑的考量,这些考量对于编写大型、复杂的系统至关重要:
- 提高代码的可读性和可推理性: 当你阅读一段陌生的 Rust 代码时,只要看到
let x = ...
,你就可以在脑海中打上一个标签:“这个变量的值在它的作用域内不会再变了”。这极大地减轻了你的心智负担,你不需要像在 Java 中那样,通读整个方法才能确定一个变量的值是否被中途修改。 - 为“无畏并发”奠定基石: 这是最重要的一点。如果一份数据是不可变的,那么在多线程环境下共享它就是绝对安全的。你根本不需要担心数据竞争(Data Race)的问题,因为没有任何线程能修改它。默认不可变性是 Rust 实现“无畏并发”的第一块,也是最重要的一块基石。
- 让编译器进行更激进的优化: 当编译器知道一个值不会改变时,它可以进行更多的优化操作,比如将值直接内联,从而产生更高效的机器码。
请求可变性:mut
关键字
那么,如果我确实需要一个可变的变量呢?(比如循环中的计数器)。Rust 当然提供了这个选项,这正是编译器提示我们的 mut
关键字。mut
是 mutable(可变的)的缩写。
让我们遵循编译器的建议,修复我们的代码:
fn main() {
let mut x = 5; // 在变量名前加上 mut
println!("The value of x is: {}", x);
x = 6; // 现在,这行代码完全合法了!
println!("The value of x is: {}", x);
}
再次 cargo run
,程序完美运行,并输出:
The value of x is: 5
The value of x is: 6
在 Rust 的世界里,使用 mut
不仅仅是一个语法要求,它更是一个明确的意图表达。当你写下 mut
时,你是在告诉自己、告诉你的同事、也告诉编译器:“注意,这个变量的状态将会改变,请对它保持警惕。” 这种明确性使得代码的维护和重构都变得更加安全。
一个进阶话题:变量遮蔽(Shadowing)
谈到变量,Rust 还有一个有趣且强大的特性,叫做“遮蔽”(Shadowing),这在 Java 中是没有直接对应概念的。看下面的代码:
fn main() {
let x = 5;
let x = x + 1; // 这里,我们再次使用 let 声明了 x
{
let x = x * 2; // 在内部作用域,我们又声明了一次 x
println!("The value of x in the inner scope is: {}", x); // 输出 12
}
println!("The value of x is: {}", x); // 输出 6
}
这段代码是完全合法的。这里发生了什么?我们使用 let
关键字重复声明了同名变量 x
。第二次的 let x
创建了一个全新的变量,它“遮蔽”了第一个 x
。在内部作用域中,第三个 let x
又遮蔽了第二个。当内部作用域结束,遮蔽也随之结束,x
的值回到了 6。
这和 mut
有什么本质区别?
let mut x = 5; x = 6;
是在改变同一个内存地址上存储的值,且变量的类型不能改变。let x = 5; let x = x + 1;
是创建了一个全新的变量,它恰好和旧变量同名。因为是新变量,我们甚至可以改变它的类型!这在 Java 中是不可想象的。
let spaces = " "; // spaces 是一个字符串切片 &str 类型
let spaces = spaces.len(); // spaces 现在被遮蔽为一个全新的变量,类型是 usize (一个整数)
这个特性非常实用,它允许我们在不引入新变量名(如 spaces_str
, spaces_num
)的情况下,对一个值进行一系列变换。
别忘了常量:const
最后,提一下常量(Constants)。它和 final
变量有些相似,但更加严格。
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
const
和 let
的不可变变量有什么区别?
const
必须在声明时注明类型(如u32
)。const
只能被设置为一个常量表达式,而不能是任何需要在运行时才能计算出的值。它在编译期间就已被“写死”在程序里。const
可以在任何作用域中声明,包括全局作用域。
你可以把它理解为 Java 中的 public static final int ...
,是一个真正意义上的、全局的、编译期常量。
本章小结
我们今天深入探讨了 Rust 中最基础的构件——变量。但我们看到的远不止语法。我们见证了一场从 Java 到 Rust 的思维转变:
从“可变为常态,不可变为选择” 到 “不可变为常态,可变为选择”。
这个看似微小的差异,是 Rust 安全大厦的奠基石。它鼓励我们编写更可预测、更易于推理、且为并发编程做好了准备的代码。let
的不可变性给了我们信心,而 mut
则成了我们表达“此处需要特别注意”的清晰信号。
现在我们理解了变量本身的行为。但是,这些变量所“持有”的数据,比如一个简单的 i32
,一个 String
,在内存中又是如何存在的呢?当我们把一个变量赋值给另一个变量时,背后又发生了什么?这正是所有权系统开始闪耀光芒的地方。
在下一章 《数据类型——不只是 int
和 String
》 中,我们将探索 Rust 的数据世界,并首次近距离观察,当数据与所有权规则相遇时,会碰撞出怎样有趣的火花。