目标:
搞懂 let、let mut、const、static、shadowing,以及它们背后的设计哲学。
一、核心概念
- 变量默认不可变
Rust 和其他语言最大的不同:变量默认不可变。
rust
fn main() {
let x = 5;
x = 6; // ❌ 编译错误:cannot assign twice to immutable variable
}
想改就得加 mut:
rust
fn main() {
let mut x = 5;
x = 6; // ✅
println!("{}", x); // 6
}
为什么这样设计? 因为大部分 bug 来自"意料之外的修改"。默认不可变让编译器帮你挡住这类问题,需要改时再显式声明,意图清晰。
- 常量 const
rust
const MAX_POINTS: u32 = 100_000;
规则:
· 必须标注类型
· 值必须是编译期可计算的常量表达式
· 命名惯例:全大写加下划线
· 可以定义在任意作用域,包括全局
· 不能用 mut
- 静态变量 static
rust
static GREETING: &str = "Hello";
static mut COUNTER: i32 = 0; // 可变的 static 是 unsafe 的,先别碰
const 和 static 的区别:
· const 在编译期被"内联"到使用处,相当于文本替换
· static 有固定内存地址,全局唯一
· 大多数场景用 const 就够
- 变量遮蔽(shadowing)
可以用同一个名字重新 let,新变量会"遮蔽"旧变量:
rust
fn main() {
let x = 5;
let x = x + 1; // 新的 x,值为 6
let x = x * 2; // 又一个新的 x,值为 12
println!("{}", x); // 12
}
遮蔽 vs mut 的关键区别:
· mut 是同一个变量,类型不能变
· 遮蔽是新变量,可以换类型
rust
let spaces = " "; // &str
let spaces = spaces.len(); // usize,类型变了,合法
而用 mut 就不行:
rust
let mut spaces = " ";
spaces = spaces.len(); // ❌ 类型不匹配
二、多元化练习题(含答案与解读)
🟢 练习 1(基础):让代码编译通过
rust
fn main() {
let x = 10;
x = 20;
println!("{}", x);
}
参考答案
rust
fn main() {
let mut x = 10;
x = 20;
println!("{}", x); // 20
}
解读:只需把 let x 改成 let mut x。编译器报错信息会明确提示 "help: consider changing this to be mutable"。
🟢 练习 2(基础):常量定义
要求:定义一个常量 PI,值为 3.14159,类型 f64,并打印它。
参考答案
rust
const PI: f64 = 3.14159;
fn main() {
println!("PI = {}", PI);
}
解读:常量必须标类型,命名全大写。常量可以定义在 main 外面,全局可用。
🟡 练习 3(理解 shadowing):预测输出
rust
fn main() {
let x = 1;
let x = x + 1;
{
let x = x * 10;
println!("内层: {}", x);
}
println!("外层: {}", x);
}
参考答案
内层: 20
外层: 2
解读:花括号 {} 创建了一个新的作用域。内层的 let x = x * 10 只在内层生效,出了花括号就回到外层的 x = 2。这就是遮蔽加作用域的组合效果。
🟡 练习 4(遮蔽换类型):修复代码
要求:下面代码想先存字符串长度,再存字符串本身,但顺序反了导致类型冲突。请用 shadowing 修复。
rust
fn main() {
let s = "hello".len(); // usize
let s = "hello"; // &str,想改成这个顺序
println!("{}", s);
}
参考答案
rust
fn main() {
let s = "hello"; // &str
let s = s.len(); // usize,类型变了,合法
println!("{}", s); // 5
}
解读:shadowing 允许变量换类型。如果这里用 mut,第一次 let s = "hello" 后类型就锁定为 &str,再赋 usize 会编译失败。
🟡 练习 5(mut vs shadowing):判断对错
以下两段代码,哪段能编译?为什么?
A:
rust
fn main() {
let mut x = 5;
x = "hello";
}
B:
rust
fn main() {
let x = 5;
let x = "hello";
}
参考答案
A 编译失败,B 编译通过。
解读:A 用 mut,x 的类型在第一次赋值时就锁定为整数,后面赋字符串类型不匹配,报错 mismatched types。B 用 shadowing,第二次 let x 创建了一个全新的变量,类型可以自由变。
🔴 练习 6(综合):计算圆面积
要求:
· 定义常量 PI: f64 = 3.14159
· 定义变量 radius,初始为 2.0,可修改
· 先把 radius 改为 3.0
· 用 shadowing 创建 area = PI * radius * radius
· 打印面积
参考答案
rust
const PI: f64 = 3.14159;
fn main() {
let mut radius = 2.0;
radius = 3.0;
let area = PI * radius * radius;
println!("面积 = {:.2}", area); // 面积 = 28.27
}
解读:radius 需要改,所以用 mut。area 只算一次不再改,用普通 let。{:.2} 是格式化占位符,保留两位小数。
🔵 思考题 1:为什么 Rust 默认不可变?
参考答案
三个层面的理由:
安全性:大多数并发 bug 和逻辑 bug 来自意外修改。默认不可变让编译器在编译期拦截这些问题。
可读性:看到 let x = 5 就知道 x 不会变,读代码时不用追踪后续所有赋值。看到 let mut x 就知道要小心,修改意图是显式的。
性能:不可变数据更容易优化,编译器可以放心做内联、缓存。同时不可变引用可以安全地跨线程共享。
解读:这是 Rust "零成本抽象"和"安全优先"哲学的体现。不是限制你,而是让你在需要可变时才付可变的代价。
🔵 思考题 2:const 和 static 到底该用哪个?
参考答案
优先用 const。
const 是编译期常量,使用时被内联到代码里,没有固定内存地址,适合大多数"配置值"场景,比如 const MAX: u32 = 100;。
static 有固定内存地址,全局唯一。适合需要取地址、需要跨线程共享、或者值很大不想被多处复制的情况。
rust
const NAME: &str = "Rust"; // 推荐
static GREETING: &str = "Hello"; // 需要地址时才用
解读:新手 99% 的场景用 const 就够了。static mut 是 unsafe 的,涉及全局可变状态,除非必要不要碰,后面讲并发时会再提到。
🔵 思考题 3:shadowing 会带来性能开销吗?
参考答案
不会。
shadowing 只是创建了一个新的栈上变量(或直接复用寄存器),编译器会做优化。旧的变量如果之后不再使用,会被直接释放或优化掉。
实际上,shadowing 常常让代码更清晰,比如把字符串转成数字:
rust
let input = "42";
let input: i32 = input.parse().unwrap();
比 let input_str = ...; let input_num = ... 更简洁,也避免了一堆 _str、_num 后缀。
解读:shadowing 是 Rust 里非常常用的惯用法,尤其在类型转换、数据加工链中。放心用。
三、知识点总结(第2课)
默认不可变:let x = 5 后不能改,需要改用 let mut x = 5。
常量:const NAME: Type = value;,必须标类型、全大写、编译期可计算。
静态变量:static,有固定地址,全局唯一,多数场景用 const 替代。
变量遮蔽:同名 let 创建新变量,可换类型,是惯用法。
mut vs shadowing:mut 同一变量类型不变;shadowing 新变量类型可变。
作用域:花括号 {} 创建新作用域,内层遮蔽不影响外层。
格式化:{:.2} 保留两位小数,{} 默认填充。
四、下一课预告
第3课:数据类型
· 标量类型:整数、浮点、布尔、字符
· 复合类型:元组、数组
· 类型推断与显式标注
· 整数溢出与 checked_* 系列方法
📌 本课心法:
不可变是默认,可变是选择。每写一个 mut,都是在向编译器声明"我知道这里会变,我为此负责"。