3. 变量与可变性:从let到所有权初探

好,咱们今天来聊聊Rust里最基础、但也最容易让人迷糊的一块——变量与可变性。说实话,我当年刚接触Rust时,最不适应的就是它这套变量规则。别的语言里,变量就是变量,想改就改。但在Rust里,你得先搞清楚:它是可变的吗?它的生命周期有多长?它是不是被“移动”了?

别急,咱们一步步来。这一章我会带你搞懂四个核心概念:letmut、变量遮蔽、常量与静态变量、以及作用域与所有权的初步认识。


3.1 let与mut:默认不可变

在Rust里,用let声明的变量,默认是不可变的。什么意思?就是你一旦给它赋了值,就不能再改了。

let x = 5;
x = 6; // 编译错误!

我第一次写这段代码时,心里想的是:“这编译器是不是有病?” 但后来我明白了——这是Rust为了安全而设计的。不可变性意味着数据不会被意外修改,这在并发编程里尤其重要。

那如果我真的需要修改呢?很简单,加个mut关键字:

let mut y = 5;
y = 6; // 没问题
我的习惯: 我一般先写let,不加mut。等编译器报错了,再回头加。这样能强迫自己思考:这个变量真的需要变吗?很多时候,其实不需要。

你可能会问:“那性能呢?不可变变量会不会更慢?” 恰恰相反。编译器可以对不可变数据做更多优化。说白了,你告诉编译器“这东西不会变”,编译器就能放心大胆地优化它。


3.2 变量遮蔽(Shadowing)

这个特性挺有意思。你可以用同一个变量名,声明一个新的变量,覆盖掉旧的。这叫“遮蔽”。

let x = 5;
let x = x + 1; // 遮蔽了之前的x
let x = x * 2; // 再次遮蔽
println!("{}", x); // 输出12

注意,这里我们没有用mut。每次let x = ...都是创建了一个新的变量,只是名字相同而已。旧的x被“遮蔽”了,但并没有被销毁——它只是暂时不可见了。

我在项目中遇到过一种场景:从用户输入读取一个字符串,然后解析成数字。用遮蔽就很方便:

let input = String::from("42");
let input: u32 = input.parse().unwrap(); // 遮蔽,类型也变了

你看,变量名没变,但类型从String变成了u32。这在其他语言里很难做到。

我曾经踩过的坑: 遮蔽不是修改!它创建的是新变量。如果你在循环里用遮蔽,每次迭代都会创建一个新变量,旧的那个会被丢弃。这可能导致意外的内存分配。所以,遮蔽虽好,但别滥用。

3.3 常量(const)与静态变量(static)

这两个概念容易混淆。咱们来理一理。

常量(const)

常量用const声明,值在编译时就确定了。它永远不可变,而且必须标注类型。

const MAX_POINTS: u32 = 100_000;

常量可以在任何作用域声明,包括全局。它的值会被内联到使用它的地方——说白了,就是编译器会把所有用到MAX_POINTS的地方,直接替换成100_000

静态变量(static)

静态变量用static声明,也有固定内存地址。它也是全局可访问的。

static APP_NAME: &str = "Rust Course";

静态变量和常量的区别在于:

特性 const static
内存位置 内联到代码中 固定内存地址
可变性 永远不可变 默认不可变,可用mut(但需要unsafe)
类型标注 必须 必须
使用场景 魔法数字、配置值 全局状态、单例
我的建议: 能用const就别用static。静态变量容易引发全局状态问题,尤其在多线程环境下。我见过一个项目,因为滥用static mut,导致数据竞争,查了三天才找到bug。

3.4 作用域与所有权初探

好,终于到了Rust最核心的概念——所有权。咱们先不深入,只做个初步了解。

在Rust里,每个值都有一个所有者。当所有者离开作用域时,值会被自动释放。这就是Rust的内存管理方式——没有垃圾回收,全靠编译器在编译时检查。

{
    let s = String::from("hello"); // s进入作用域
    // 使用s
} // 作用域结束,s被释放

你想想看,这跟C++的RAII很像。但Rust更严格——它不允许有两个所有者同时指向同一块内存。这就是“所有权”的核心规则:

  1. 每个值有且只有一个所有者
  2. 当所有者离开作用域,值被丢弃
  3. 所有权可以转移(移动),但不能复制(除非类型实现了Copy trait)

举个例子:

let s1 = String::from("hello");
let s2 = s1; // 所有权从s1移动到s2
// println!("{}", s1); // 编译错误!s1已经无效了

我第一次看到这个错误时,心里想的是:“什么鬼?我明明只是赋值啊!” 但在Rust里,赋值默认是移动,不是复制。这跟C++的拷贝语义完全不同。

避坑指南: 如果你真的需要复制,可以显式调用.clone()方法。但要注意,clone是深拷贝,对性能有影响。我一般只在必要时才用。

作用域和所有权的关系,可以用下面这张图来理解:

外层作用域 内层作用域 s1: String 移动 s2: String ✕ 内层结束,s2被释放 ✕ 外层结束,s1已无效

嗯,这张图展示的就是:s1的所有权移动到了s2,然后s2在内层作用域结束时被释放。而s1因为已经失去了所有权,所以在外层作用域结束时,什么都不会发生。


小结

这一章我们聊了:

  • let与mut:默认不可变,需要修改时加mut
  • 变量遮蔽:用同名新变量覆盖旧变量,类型可以不同
  • 常量与静态变量const内联,static有固定地址,尽量用const
  • 作用域与所有权:每个值只有一个所有者,离开作用域自动释放

说实话,所有权这个概念,刚开始确实有点反直觉。但一旦你习惯了,就会发现它让代码更安全、更可预测。我在写Rust半年后,再回头看自己写的C++代码,简直不敢直视——到处都是潜在的内存泄漏和野指针。

好了,这一章就到这里。记住:变量默认不可变,所有权只能有一个。这两个规则,是Rust安全性的基石。

专注资料整理