在上一章里,我们已经见识了 Rust 的第一次"暴击"——没有 GC 却能保证内存安全。现在,准备好迎接第二次冲击吧。
你以为变量声明是最简单的事情?在 Java 里确实如此:int x = 5;
然后想怎么改就怎么改。但在 Rust 的世界里,连声明个变量都要重新学习。
这不是 Rust 在故意为难你,而是它在用最基础的语法特性,向你传递一个颠覆性的编程哲学。
第一课:Rust 说"不"
废话少说,我们直接上代码。创建一个新项目:
cargo new variables
cd variables
在 src/main.rs
里写下这几行代码:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6; // 试试看能不能改个值
println!("The value of x is: {}", x);
}
这代码有什么问题吗?在 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 编译器不仅告诉你错在哪里,还教你怎么修复。这就是传说中的"编译器老师"。
错误信息的核心:cannot assign twice to immutable variable
。
翻译过来就是:“你想改变一个不可变的变量?做梦!”
这不是 Bug,这是哲学
在 Java 里,变量默认就是可变的:
int x = 5;
x = 6; // 随便改,没人管你
想要不可变?你得加个 final
:
final int x = 5;
x = 6; // 这才会报错
但在 Rust 里,逻辑完全反过来了:
- Java 哲学:变量默认可变,你想要安全就自己加
final
- Rust 哲学:变量默认不可变,你想要危险就明确声明
这不是语法差异,这是设计哲学的根本分歧。
Rust 为什么要这么"独断专行"?三个字:为了命。
为了命的设计
想象一下,你在维护一个百万行代码的系统。你看到这样一行代码:
// Java 代码
User user = getUser(id);
现在问你:在接下来的 200 行代码里,user
这个变量会不会被修改?
你不知道。
你必须把这 200 行代码全部读完,才能确定 user
是否被"动过手脚"。
但在 Rust 里:
// Rust 代码
let user = get_user(id);
看到这行,你立刻就知道:user
在整个作用域内都不会变。这不是约定,这是编译器的保证。
这种确定性有三个巨大的好处:
- 代码更容易理解:不用担心变量被"偷偷修改"
- 并发更安全:不可变数据可以在多线程间安全共享
- 编译器优化更激进:确定不变的数据可以内联优化
当你真的需要改变时:mut
关键字
好吧,总有些时候你确实需要修改变量(比如循环计数器)。Rust 当然提供了这个选项,但你必须明确表达意图:
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
成功了!
但注意这里的深层含义:mut
不只是一个语法要求,它是一个明确的意图声明。
当你写下 let mut x
时,你是在告诉:
- 自己:这个变量会变化,要小心
- 同事:看到这个变量时要留意它的状态变化
- 编译器:请对这个变量的所有修改保持警惕
这种明确性让代码的维护变得更安全,重构变得更容易。
进阶技巧:变量遮蔽(Shadowing)
接下来这个特性会让你大开眼界。在 Java 里,你不能在同一个作用域声明两个同名变量。但 Rust 可以:
fn main() {
let x = 5;
let x = x + 1; // 又声明了一个 x?没错!
{
let x = x * 2; // 在内部作用域再来一次
println!("Inner scope x: {}", x); // 输出 12
}
println!("Outer scope x: {}", x); // 输出 6
}
这代码居然能编译通过!
这就是"变量遮蔽"(Shadowing)。每次使用 let
声明同名变量时,你实际上创建了一个全新的变量,它"遮蔽"了之前的变量。
Shadowing vs Mut:本质差异
mut
和 shadowing
看起来都能"改变"变量,但本质完全不同:
// mut:修改同一个内存位置的值
let mut x = 5;
x = 6; // 改变值,类型不能变
// shadowing:创建新变量
let x = 5;
let x = 6; // 创建新变量,类型可以变
更强大的是,shadowing 允许改变类型:
let spaces = " "; // 字符串类型
let spaces = spaces.len(); // 数字类型,完全合法!
在 Java 里这是不可想象的。你必须声明两个不同的变量名:spacesStr
和 spacesLen
。
常量:终极不可变
最后说说常量。如果说 let
是"默认不可变",那么 const
就是"绝对不可变":
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
const
和 let
的区别:
const
必须标注类型const
只能是编译期常量表达式const
可以在全局作用域声明const
在编译时就被"硬编码"到程序里
你可以把它理解为 Java 的 public static final
。
写在最后:思维的转换
今天我们见证了一个看似简单的概念背后的深刻哲学转变:
从"默认可变,选择不可变" 到 “默认不可变,选择可变”
这不是语法糖,这是安全第一的设计哲学。它强迫你思考:
- 这个变量真的需要改变吗?
- 如果需要改变,在哪里改变?
- 改变会带来什么风险?
当你开始习惯这种思维方式时,你会发现自己写出的代码更加:
- 可预测:看到
let
就知道不会变 - 易维护:
mut
明确标记了"危险区域" - 并发友好:不可变数据天然线程安全
现在你理解了变量的行为。但这些变量指向的数据在内存中是怎么存储的?当你把一个变量赋值给另一个变量时,发生了什么?
这就是我们下一章要探讨的内容:数据类型,以及它们与所有权系统的第一次"亲密接触"。
准备好了吗?因为真正的挑战才刚刚开始。