3. 变量与可变性:let与mut,常量const,变量遮蔽(shadowing),作用域与所有权初探

好,咱们进入第三章。说实话,这一章是 Rust 里最「反直觉」的部分之一。我刚从 C++ 转过来时,被编译器怼得满头包。但别怕,一旦你理解了 Rust 的设计哲学,你会爱上这种严谨。

3.1 默认不可变:let 与 mut

在 Rust 里,变量默认是不可变的。什么意思?就是你声明了一个变量后,不能改它的值。

fn main() {
    let x = 5;
    x = 6; // 编译错误!
}

我第一次写这段代码时,心想:「什么鬼?变量不就是用来变的吗?」但 Rust 的设计者不这么想。他们觉得,默认不可变能让代码更安全、更容易推理。

那怎么让变量可变呢?加个 mut 关键字:

fn main() {
    let mut x = 5;
    x = 6; // 没问题
    println!("x = {}", x);
}
我的习惯: 我一般先写 let,等编译器报错了再加 mut。这样能逼自己思考:「这个变量真的需要变吗?」很多场景下,其实不需要。

3.2 常量:const

常量跟不可变变量有点像,但有本质区别。常量用 const 声明,且必须标注类型:

const MAX_POINTS: u32 = 100_000;

注意几个点:

  • 常量在编译时就确定了值
  • 常量可以在任何作用域声明,包括全局
  • 常量不能加 mut,它永远不可变

我在项目中常用常量来定义配置值,比如超时时间、最大连接数。这样改配置时只需要改一处,不会到处找魔法数字。

注意: 常量与不可变变量不同。变量可以用 let 重复声明(遮蔽),但常量不行。常量名在整个作用域内必须唯一。

3.3 变量遮蔽(Shadowing)

这个特性很有意思。你可以用同一个变量名声明新变量,新变量会「遮蔽」旧变量:

fn main() {
    let x = 5;
    let x = x + 1; // 遮蔽,x 现在是 6
    let x = x * 2; // 再次遮蔽,x 现在是 12
    println!("x = {}", x);
}

遮蔽跟 mut 不同。用 mut 是修改同一个变量的值,而遮蔽是创建了一个全新的变量。这意味着你可以改变变量的类型:

let spaces = "   ";
let spaces = spaces.len(); // 从字符串变成了数字

如果用 mut,上面这段代码会报错,因为类型不匹配。但遮蔽允许你「重新绑定」一个不同类型的值到同一个名字上。

我曾经在解析配置文件时用过这个技巧。先读成字符串,然后遮蔽成数字或布尔值,代码看起来干净多了。

3.4 作用域与所有权初探

作用域,说白了就是变量在哪儿有效。Rust 的作用域用花括号 {} 界定:

fn main() {
    let x = 5; // x 从这里开始有效
    {
        let y = 10; // y 只在这个块内有效
        println!("x = {}, y = {}", x, y);
    } // y 在这里失效
    // println!("y = {}", y); // 编译错误!
} // x 在这里失效

这跟其他语言差不多。但 Rust 在这里引入了所有权(Ownership)的概念——这是它最核心、也最独特的设计。

所有权三条规则:

  1. 每个值都有一个所有者(owner)
  2. 同一时间只有一个所有者
  3. 所有者离开作用域时,值被丢弃

看个例子:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // 所有权从 s1 转移到了 s2
    // println!("{}", s1); // 编译错误!s1 已经失效
    println!("{}", s2); // 没问题
}

为什么?因为 String 类型的数据存在堆上,复制整个数据代价太大。Rust 选择「移动」所有权,而不是复制。这样既高效又安全。

核心思想: 所有权让 Rust 不需要垃圾回收器,就能保证内存安全。编译器在编译时就检查清楚了,运行时没有额外开销。

如果你真的需要复制,可以显式调用 clone()

let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2); // 两个都有效

但要注意,clone() 会深拷贝数据,有性能开销。我一般只在确实需要独立数据时才用。

对于整数、布尔、浮点数这类「标量类型」,它们实现了 Copy trait,赋值时会自动复制,不会转移所有权:

let x = 5;
let y = x; // 复制,不是移动
println!("x = {}, y = {}", x, y); // 两个都有效

嗯,这里要记住:Copy 类型都是存在栈上的,复制成本很低。而堆上的类型(如 StringVec)默认是移动语义。

知识体系图

下面这张图帮你理清本章的核心逻辑:

变量与可变性知识体系 变量与可变性 let 与 mut 常量 const 变量遮蔽 作用域与所有权初探 每个值只有一个所有者 所有者离开作用域即丢弃 移动 vs 复制 核心:编译时内存安全,无垃圾回收

这张图把四个概念串起来了。你会发现,所有权是 Rust 的基石,而变量遮蔽、不可变性这些设计,都是为了让所有权系统更易用、更安全。

好了,这一章的内容就这些。记住:多写、多编译、多看错误信息。Rust 的编译器是你最好的老师——它骂你骂得越狠,你学得越快。


专注资料整理