Rust 的变量系统比大多数语言多了一个维度:可变性由你显式控制,而不是默认允许修改。
声明与绑定
Rust 用 let 关键字声明变量。“变量绑定”这个名字比”变量赋值”更准确——你是在把一个值绑定到一个名字上。
基本语法
fn main() {
let x = 5; // 整数,编译器推断为 i32
let y = 3.14; // 浮点,推断为 f64
let z: u8 = 255; // 也可以显式标注类型
let flag = true; // 布尔值
println!("x={} y={} z={} flag={}", x, y, z, flag);
}类型推断:Rust 的编译器非常聪明,大多数情况下能从赋值和使用方式推断出变量类型,你不需要每次都写类型注解。当推断不了时,编译器会直接报错告诉你需要补上。
默认不可变
Rust 的变量默认是不可变的——绑定之后,值就不能再改变:
fn main() {
let x = 5;
x = 6; // 错误!不能对不可变变量二次赋值
println!("{}", x);
}编译器会给出非常清晰的错误信息,甚至告诉你解决方法:
Compiling playground v0.0.1 (/playground)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:3:5
|
2 | let x = 5;
| - first assignment to `x`
3 | x = 6; // 错误!不能对不可变变量二次赋值
| ^^^^^ cannot assign twice to immutable variable
|
为什么要默认不可变?
这是 Rust 最有意思的设计决策之一,值得认真理解。
问题场景:假设你的程序里有这样一段逻辑——
fn calculate_tax(income: f64) -> f64 {
let rate = 0.20; // 税率 20%,"感觉"不会变
// 假设中间有很多逻辑……
let taxable = income * 0.8;
//...很多行代码...
// 某个地方悄悄修改了 rate(比如另一个同事写的)
// rate = 0.25; // 如果是可变的,这行可能潜伏在几百行之后
//...很多行代码...
taxable * rate // 这里用的是哪个 rate?
}
fn main() {
println!("税额: {:.2}", calculate_tax(100_000.0));
}在大型项目中,rate 可能在函数的前半段设置,在几百行之后的某处被意外修改,导致最终计算结果出错。追踪这类 bug 非常痛苦——你不得不在整个函数里搜索”谁改了这个值”。
Rust 的解法:变量默认不可变。如果 rate 不需要改变,就不加 mut——编译器保证它不会被任何地方修改,你读代码时可以完全放心地说”这个值从声明到用完都是 0.20”。
fn main() {
let config_value = 42; // 配置项,不应该被修改
// 几百行之后,某处意外尝试修改它……
config_value = 99; // 编译器:不行!
println!("{}", config_value);
}不可变性 ≠ 性能损失:不可变变量和可变变量在运行时没有性能差异,不可变只是编译期的约束。
mut是你告诉编译器”我真的需要修改这个值”的明确声明,而不是一个优化开关。
先声明,后初始化
可以先声明变量,稍后再给它赋值——但 Rust 绝不允许使用未初始化的变量:
fn main() {
let result; // 只声明,不赋值
{
let base = 4;
result = base * base; // 在内层作用域里初始化
}
println!("result = {}", result); // 可以使用,因为已经初始化了
}fn main() {
let x;
println!("{}", x); // 错误!使用了未初始化的变量
x = 1;
}这和 C 语言不同。C 允许使用未初始化的变量(值是不确定的垃圾数据),这是很多 bug 的来源。Rust 编译器在编译期就禁止这种情况,彻底杜绝了”读垃圾值”的问题。
用 _ 前缀抑制未使用警告
声明了但没有使用的变量,编译器会发出警告。如果某个变量是有意不使用的(比如调试时临时写的),加上 _ 前缀可以告诉编译器”我知道,不用提醒我”:
fn main() {
let _intentionally_unused = 42; // 不会警告
let also_unused = 99; // 会警告:unused variable
println!("只用这一个");
// also_unused 从未被读取
let _ = also_unused; // 用 let _ = 显式丢弃也可以
}可变与常量
用 mut 声明可变变量
在变量名前加 mut,就可以在绑定后修改它的值:
fn main() {
let mut count = 0;
count += 1;
count += 1;
count += 1;
println!("count = {}", count); // 3
}mut 不只是给编译器看的,也是给读代码的人看的——它明确传达”这个变量的值会变化”。没有 mut 的变量,阅读代码时可以放心地认为它的值从始至终不变。
用 const 声明常量
const 声明的是真正的常量,有几个与 let 不同的规则:
// 常量通常在函数外声明(全局可见),也可以在函数内
const MAX_SCORE: u32 = 100;
const SECONDS_PER_HOUR: u32 = 60 * 60; // 常量表达式,编译时计算
fn main() {
println!("满分: {}", MAX_SCORE);
println!("一小时: {} 秒", SECONDS_PER_HOUR);
}const 的特点:
- 必须标注类型:编译器不推断常量类型
- 命名约定:全大写字母 + 下划线分隔(
SCREAMING_SNAKE_CASE) - 只能是常量表达式:不能是函数调用结果或运行时才知道的值
- 在任意作用域有效:包括全局,整个程序运行期间都存在
let / let mut / const 对比
let | let mut | const | |
|---|---|---|---|
| 可变 | 否 | 是 | 否(永远) |
| 必须标注类型 | 否(推断) | 否(推断) | 是 |
| 作用域 | 块作用域 | 块作用域 | 任意(含全局) |
| 能遮蔽 | 是 | 是 | 否 |
| 典型用途 | 局部值 | 需要修改的值 | 程序常量、配置值 |
const 与不可变 let 的本质区别:
let默认不可变,但可以被遮蔽(重新绑定);const是真正的常量,不能被任何操作改变,编译器会把它内联到每个使用处。
作用域与遮蔽
作用域
变量的作用域由 {} 划定——超出大括号,变量就不再存在:
fn main() {
let outer = "外层";
{
let inner = "内层";
println!("{} 和 {}", outer, inner); // 内层可以访问外层
}
println!("{}", outer); // 正常
// println!("{}", inner); // 错误!inner 已离开作用域
}变量遮蔽
用 let 重新声明同名变量,会遮蔽(shadow)之前的变量——新变量使旧变量失效,在当前作用域内使用新值:
fn main() {
let x = 5;
println!("原始 x = {}", x); // 5
let x = x + 1; // 遮蔽:新 x = 旧 x + 1
println!("遮蔽后 x = {}", x); // 6
{
let x = x * 2; // 内层作用域遮蔽
println!("内层 x = {}", x); // 12
}
println!("离开内层后 x = {}", x); // 回到 6,内层遮蔽消失
}遮蔽有一个 mut 做不到的能力:改变变量的类型:
fn main() {
// mut 无法做到这件事(类型不能改变)
let spaces = " "; // &str 类型
let spaces = spaces.len(); // 遮蔽为 usize 类型
println!("空格数: {}", spaces);
// 如果用 mut 尝试改类型,会编译报错:
// let mut spaces = " ";
// spaces = spaces.len(); // 错误!类型不匹配
}遮蔽 vs mut 的选择:如果需要修改同一个值,用
mut;如果想对一个值做一次性转换后得到一个新的不可变绑定,用遮蔽——遮蔽后的变量默认仍是不可变的。
冻结
当一个可变变量被不可变绑定遮蔽时,在该作用域内它就被”冻结”了,不能再修改。离开该作用域后,可变性恢复:
fn main() {
let mut value = 100;
{
let value = value; // 用不可变绑定遮蔽 value,冻结它
value = 200; // 错误!value 在此作用域被冻结
}
// 离开内层作用域,冻结解除
value = 200; // 正常!
println!("{}", value);
}冻结的本质:遮蔽创建了一个新的不可变变量(名字相同),在它的作用域内,可变的外层变量被”挡住”了,无从访问。
练习题
不可变变量的错误
fn main() {
let score = 100;
score = 90;
println!("{}", score);
}
mut 的含义
const 与 let 的区别
遮蔽的能力
fn main() {
let x = "hello";
let x = x.len();
println!("{}", x);
}
变量超出作用域
fn main() {
let x = 1;
{
let y = 2;
println!("{}", x + y);
}
println!("{}", y);
}
未初始化变量
编程练习 1
下面的代码想实现一个简单的计数器,但有编译错误,请修复它:
fn main() {
let count = 0;
count = count + 1;
count = count + 1;
count = count + 1;
println!("count = {}", count);
}编程练习 2
用遮蔽把字符串 " Rust " 分三步处理:先去掉首尾空白,再转换为大写,最后输出长度。每一步用同名变量 s 遮蔽,不使用 mut。
fn main() {
let s = " Rust ";
// TODO: 第一步:s = s.trim()(去掉首尾空白)
// TODO: 第二步:s = s.to_uppercase()(转大写)
// TODO: 第三步:s = s.len()(取长度)
println!("{}", s);
}