Rust 的新手期,几乎每个人都会在 mut 和遮蔽(shadowing)这两个概念上卡一阵子。我第一次看到 let x = x + 1; 这种写法时,第一反应是"Rust 不是讲究默认不可变吗?怎么还能给自己赋值?"直到后来才想明白,这行代码里同时发生了两件事:右侧的 x 是读取旧绑定的值,而左侧的 let x 是在当前作用域里创建了一个全新的绑定,把旧名字盖住了。这和 let mut x = 5; x = 6; 是两套完全不同的机制,只是表面上看着都像"给变量重新赋值"。
这篇文章就把这两个概念掰开揉碎讲清楚,适合正在学 Rust 所有权、开始写实际代码的初学者,也适合那些已经写了几个月、遇到奇怪编译错误时不太确定该选哪种写法的开发者。核心就一句话:mut 是给同一个绑定"原地改值"的权限,遮蔽是"新建一个绑定、把旧名字暂时盖住",两者在内存语义、类型规则、作用域行为上都有本质区别。搞懂了这一点,比背十遍语法规则都有用。
1. mut的本质:同地址原地写,类型被锁死
1.1 赋值运算符背后的内存操作
先看最基础的例子:
rust复制let mut count = 0;
count += 1;
count += 1;
println!("count = {}", count); // 2
mut 做的事情很纯粹:它允许你在一个绑定存活期间,通过 = 把新值写到这个绑定对应的内存位置上。注意"绑定对应的内存位置"这个说法——mut 不是换内存,而是在同一块内存上做覆盖写。你可以用 &x 打印地址来验证:
rust复制let mut x = 5;
println!("{:p}", &x); // 例如 0x7ffd...
x = 6;
println!("{:p}", &x); // 同一个地址
两次打印的地址是一样的,因为 x 始终是同一个绑定,x = 6 只是在它的内存位置上执行了一次写入。你可以把 mut 理解成操作权限上的开关:打开之后,允许往这块内存里写新值,但屋子本身的位置不变。
这里有个很多人忽略的细节:x = 6 这条语句并没有触发任何新绑定,也没有调用析构函数去处理旧值。它只是把 5 这个旧位模式覆盖成 6 的位模式。对于实现了 Drop 的类型(比如 String),用 mut 直接赋值时,旧值会被正确地析构释放,但这和遮蔽里"旧绑定等待离开作用域再析构"的时机完全不同。后面讲遮蔽时你会看到这个差异有多重要。
1.2 mut不能换类型,这是硬约束
mut 有一个非常容易被忽略的边界:绑定的类型在声明时确定,之后不能更改。
rust复制let mut spaces = " ";
spaces = spaces.len(); // 编译错误:expected `usize`, found `&str`
这个错误是 Rust 类型系统在保护你:spaces 绑定被声明为 &str 类型,而 spaces.len() 返回 usize,类型对不上,直接编译失败。这其实是一个非常关键的安全设计——如果允许同一个绑定在保持地址不变的情况下切换类型,那读取方就完全不知道该按什么布局去解读这块内存了,内存安全的大厦会瞬间倒塌。
类型不变、地址不变、只改值,这就是 mut 的全部能力边界。它解决的是"同一类型的数据需要反复修改"的问题,比如计数器、累加器、状态标志位。如果你想在保持名字不变的情况下切换类型,mut 帮不了你,那是遮蔽的地盘。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 遮蔽的机制:名字重绑定,旧绑定没被销毁
2.1 let不是赋值,是创建新绑定
遮蔽的写法看起来像赋值,但语义完全不同:
rust复制let x = 5;
let x = x + 1;
println!("{}", x); // 6
第二行代码实际上执行了三个步骤:先读取旧 x 的值(5),计算 5 + 1 得到 6,然后创建一个全新的绑定,名字也叫 x,绑定到 6 上。从这一刻起,在当前作用域内所有对 x 的引用都指向这个新绑定,旧绑定虽然还存在于内存里,但已经"隐身"了。
你可以把遮蔽想象成换门牌号登记:还是在这条街上,但门牌号"101
