Rust 变量与可变性
这一篇讲 Rust 的变量声明。Rust 在变量上有一个非常独特的设计——默认不可变。这听起来像是把人绑住,实际上它是 Rust 安全性的重要基石,逼着你写更可控的代码。理解了可变性,你才能理解后面所有权和借用为何这样设计。
1. let 声明变量
Rust 用 let 关键字声明变量。和很多语言不同,Rust 的变量默认是不可变的(immutable)——一旦赋值就不能再改。看例子:
fn main() {
let x = 5; // 默认不可变
println!("x = {}", x);
// x = 6; // 编译错误!不能给不可变变量赋值
// 要修改,必须显式加 mut 关键字
let mut y = 10;
println!("y = {}", y);
y = 20; // OK,因为声明了 mut
println!("y = {}", y);
}第一次接触会觉得反直觉:"变量怎么不能变?"。但这正是 Rust 的刻意设计。
2. 为什么默认不可变?
考虑这样一个场景:你有一个配置对象,在函数开头读进来,然后传给十几个函数处理。如果变量可变,你无法保证中间某个函数没偷偷改它——这种 bug 排查起来非常痛苦(典型的"幽灵数据"问题)。Rust 让变量默认锁死,只有显式声明 mut 才能改:
// 默认不可变的好处:防止"不小心改了不该改的值"
fn main() {
let config = load_config();
// config 在整个函数里都不会被改——可以放心
process(config); // 拿到的一定是最初的值
save(config); // 不会被中间某个函数偷偷改掉
}
// 如果某个变量确实需要变,加 mut 是"明确告诉读者:这里会变"
// 代码审查时,mut 像一个显眼的标记好处有两个:
- 阅读代码时:看到
mut就知道"这里会发生改变",眼睛会自动停留,bug 难以藏身。 - 并发安全:不可变数据天然可以多线程共享,不需要锁。Rust 的"无畏并发"正是建立在此之上。
如果你之前用 const 风格的 JS/TS(用 const 多过 let),会发现 Rust 的理念是一致的——只是更彻底,把 const 设成了默认。
3. Shadowing(遮盖):用同名变量覆盖
Rust 有一个独特的特性叫 shadowing。你可以用 let 重新声明一个同名变量,新变量会"遮盖"旧的。这和 mut 完全不同:
fn main() {
let x = 5;
let x = x + 1; // shadowing:用同名变量遮盖旧的
println!("x = {}", x); // x = 6
{
let x = x * 2; // 内层作用域再 shadow 一次
println!("内层 x = {}", x); // 12
}
println!("外层 x = {}", x); // 6,内层的 shadow 不影响外层
// shadowing 最强大的地方:可以改类型!
let spaces = " "; // &str 类型
let spaces = spaces.len(); // usize 类型,合法
// 对比:用 mut 就不能这样,因为类型不能变
// let mut count = 5;
// count = "六"; // 编译错误,类型不匹配
}shadowing 最强大的地方在于可以改变类型。比如先有一个字符串表示空格数,然后想转成数字——你可以用同名变量,而不是起一个 spaces_str、spaces_count 这种啰嗦的名字。这在"对同一份数据做逐步转换"的场景下非常优雅,是 Rust 风格的代码标志之一。
注意 shadowing 和 mut 的区别:
- mut:同一个变量,值在变,类型不能变。
- shadowing:其实是一个全新的变量,只是名字相同,类型可以变。旧的会被编译器优化掉。
4. 常量 const
常量用 const 声明,和不可变变量相似但有几个关键区别:
// 常量:用 const 声明,必须标注类型,值必须编译期可计算
const MAX_USERS: u32 = 100_000; // 数字可以用下划线分隔,易读
const PI: f64 = 3.14159265;
const GREETING: &str = "你好";
// 常量命名约定:全大写 + 下划线
// 常量可以在任何作用域声明,包括全局
// 常量不能 shadow,不能加 mut
fn main() {
println!("最多 {} 个用户", MAX_USERS);
println!("圆周率是 {}", PI);
}
// static:静态变量(有固定内存地址,生命周期等于整个程序)
static COUNTER: u32 = 0; // 不可变 static
// 可变 static 在多线程下不安全,必须包在 unsafe 里访问
static mut STATE: i32 = 0; // 极少使用,通常用原子类型替代const 与 let 的区别:
- const 必须显式标注类型;
let可以推断。 - const 的值必须是编译期就能算出来的常量表达式;
let可以是运行时计算的结果。 - const 可以在任何作用域(包括全局)声明;
let只能在函数内。 - const 编译时会内联到使用处,没有固定内存地址;
static才有固定地址。
简单记:全局不变的值用 const;运行时算出来但不再变的值用 let;需要在多处共享且固定地址的用 static(罕见)。
5. 变量作用域
Rust 的作用域规则和大多数 C 系语言一致:花括号定义作用域,变量在离开作用域时失效。这对后面理解所有权至关重要——Rust 的内存释放就是基于作用域的:
fn main() {
let a = 1; // s 在外层作用域有效
{
let b = 2; // b 只在内层作用域有效
let a = 3; // shadow 外层的 a,只在内层有效
println!("内层 a = {}, b = {}", a, b); // 3, 2
} // b 离开作用域,被释放;内层 a 也失效
println!("外层 a = {}", a); // 1,恢复成外层那个 a
// println!("{}", b); // 编译错误!b 已不存在
}当变量离开作用域,Rust 会自动调用它的"析构"(drop),释放资源。这不需要你手动写 free/close,是所有权系统的核心机制之一。下一篇讲数据类型时我们会更具体看到这一点。
6. 类型推断与显式标注
Rust 是静态类型语言,但绝大多数时候不需要写类型——编译器会从上下文推断。只有在推断不出(或想让代码更清晰)时才需要标注:
fn main() {
// 类型推断:不写类型,Rust 自动推断
let x = 5; // 推断为 i32(默认整数类型)
let y = 2.5; // 推断为 f64(默认浮点)
let z = true; // bool
let c = 'A'; // char
// 显式标注类型(类型标注用冒号)
let big: i64 = 9_000_000_000;
let ratio: f32 = 0.5;
let name: &str = "小明";
// 让编译器推断一个难以推断的类型时,标注很有用
let guess: u32 = "42".parse().expect("不是数字");
println!("{}", guess); // 42
}数字字面量默认推断为 i32(整数)和 f64(浮点),这是为了减少标注。如果实际需要别的位宽,加冒号标注即可。数字可以用下划线分隔(如 1_000_000),增强可读性,不影响值。
7. 没有声明的变量不能用
Rust 强制"先声明再使用",没有 hoisting(JS 的变量提升)这类机制。在使用一个变量前必须先 let 它,且必须初始化——未初始化的变量不能读取。这避免了"读到垃圾值"的经典 bug:
- 声明
let x;后,在给 x 赋值前使用它,编译器会直接报错。 - 这和 C 的"读到栈上随机值"完全是两个世界。
小结
这一篇你掌握了 Rust 变量的核心:默认不可变(用 mut 才能改)、shadowing(同名新变量,类型可变)、const 常量、作用域决定生命周期。最该记住的理念是:Rust 把"不可变"作为默认值,把 mut 当作一个显眼的标记。下一篇我们看数据类型——整数、浮点、元组、数组,Rust 的类型系统有几个独特之处。
← 上一篇 Rust 基础语法
下一篇 Rust 数据类型 →