第 2 章:还在为变量赋值这点小事纠结?

在上一章里,我们已经见识了 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 在整个作用域内都不会变。这不是约定,这是编译器的保证

这种确定性有三个巨大的好处:

  1. 代码更容易理解:不用担心变量被"偷偷修改"
  2. 并发更安全:不可变数据可以在多线程间安全共享
  3. 编译器优化更激进:确定不变的数据可以内联优化

当你真的需要改变时: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:本质差异

mutshadowing 看起来都能"改变"变量,但本质完全不同:

// mut:修改同一个内存位置的值
let mut x = 5;
x = 6; // 改变值,类型不能变

// shadowing:创建新变量
let x = 5;
let x = 6; // 创建新变量,类型可以变

更强大的是,shadowing 允许改变类型:

let spaces = "   ";        // 字符串类型
let spaces = spaces.len(); // 数字类型,完全合法!

在 Java 里这是不可想象的。你必须声明两个不同的变量名:spacesStrspacesLen

常量:终极不可变

最后说说常量。如果说 let 是"默认不可变",那么 const 就是"绝对不可变":

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

constlet 的区别:

  • const 必须标注类型
  • const 只能是编译期常量表达式
  • const 可以在全局作用域声明
  • const 在编译时就被"硬编码"到程序里

你可以把它理解为 Java 的 public static final

写在最后:思维的转换

今天我们见证了一个看似简单的概念背后的深刻哲学转变:

从"默认可变,选择不可变" 到 “默认不可变,选择可变”

这不是语法糖,这是安全第一的设计哲学。它强迫你思考:

  • 这个变量真的需要改变吗?
  • 如果需要改变,在哪里改变?
  • 改变会带来什么风险?

当你开始习惯这种思维方式时,你会发现自己写出的代码更加:

  • 可预测:看到 let 就知道不会变
  • 易维护mut 明确标记了"危险区域"
  • 并发友好:不可变数据天然线程安全

现在你理解了变量的行为。但这些变量指向的数据在内存中是怎么存储的?当你把一个变量赋值给另一个变量时,发生了什么?

这就是我们下一章要探讨的内容:数据类型,以及它们与所有权系统的第一次"亲密接触"。

准备好了吗?因为真正的挑战才刚刚开始。