⏱️ 阅读时长:10 分钟
🎯读完收获:理解 Rust 项目结构 + 掌握 let / let mut / 变量遮蔽
前情提要
上一篇,我们聊了为什么要学 Rust,还在电脑上跑通了第一个程序。
如果你跟着做了,现在你的电脑里应该有一个 rust_demo 文件夹,运行 cargo run 能看到角色信息的输出。
但你可能会有些疑问:
-
「
cargo new到底创建了什么?」 -
「那个
Cargo.toml文件是干嘛的?」 -
「为什么代码要写在
src/main.rs里?」 -
「
fn main()又是什么意思?」
今天,我们先把这些问题搞清楚,然后正式开始学习 Rust 的第一个核心概念:变量。
本篇目标
学完这一篇,你将掌握:
✦ Rust 项目的基本结构
✦
Cargo.toml的作用✦
fn main()为什么是程序入口✦
let和let mut的区别✦ 变量遮蔽(shadowing)的用法
我们开始吧。
第一节:认识你的修炼洞府
上一篇我们用 cargo new rust_demo 创建了项目。现在让我们看看这个命令到底创建了什么。
rust_demo/
├── Cargo.toml ← 项目配置文件
├── src/
│ └── main.rs ← 你的代码写在这里
└── target/ ← 编译产物(运行后才会出现)
这就是一个最简单的 Rust 项目结构。我喜欢把它比作修仙的「洞府」,这是你平常的工位:
让我们逐个看看这些「设施」。
Cargo.toml:你的门派典籍
打开 Cargo.toml,你会看到类似这样的内容:
[package]
name = "rust_demo"
version = "0.1.0"
edition = "2024"
[dependencies]
这个文件是整个项目的「身份证」和「资源清单」:
| 字段 | 含义 | 类比 |
|---|---|---|
| name | 项目名称 | 你的道号 |
| version | 版本号 | 你的修为等级 |
| edition | Rust 版本 | 你修炼的功法版本 |
| [dependencies] | 依赖的外部库 | 你借用的外门功法 |
现在 [dependencies] 是空的,因为我们还没用到任何外部库。后面需要时会在这里添加。
你暂时不需要修改这个文件,知道它的作用就行。
src/main.rs:你的修炼室
所有的 Rust 代码都写在 src/ 目录下。对于我们这个简单项目,只有一个文件 main.rs。
为什么叫 main.rs?
因为 Rust 规定:可执行程序的入口文件必须叫 main.rs。这就像修仙门派规定:「主修炼室必须建在洞府正中央,以便聚集灵气」。
fn main() {
// 业务代码
}
上一篇我们的代码就写在这里,我们了解下这个部分
| 部分 | 含义 |
|---|---|
| fn | 「function」的缩写,表示定义一个函数 |
| main | 函数的名字,必须叫 main |
| () | 参数列表,这里是空的 |
| { } | 函数体,你的代码写在这里面 |
为什么函数必须叫 main?
这是 Rust(以及很多其他语言)的约定:程序启动时,操作系统会去找一个叫 main 的函数,然后从那里开始执行。
就像修仙世界的规矩:「功法必须从『吐纳基础』开始。」不是因为这个名字有什么魔力,而是大家都遵守这个约定。
target/:丹药库(编译产物)
当你运行 cargo run 或 cargo build 后,会出现一个 target/ 文件夹。里面存放的是编译后的文件。
你通常不需要关心这个文件夹。如果想清理它(比如项目太大了),可以运行:
cargo clean
这里是一些伴随着项目构建产生的文件,生成的可执行文件最后会出现在这里
好,现在你知道代码应该写在哪里、程序是如何运行的了。
接下来,让我们正式开始修炼第一门功法。
第二节:山门考验——你的灵气能改变吗?
你来到山门前,一位守门师兄拦住了你。
> 「想要入门?先回答我一个问题。」
> 「修仙之人,灵气是根本。但灵气有两种——恒定灵气与流动灵气。」
> 「恒定灵气,一旦凝聚,便不可更改,稳如磐石。」
> 「流动灵气,可增可减,灵活多变,但也容易失控。」
> 「你觉得,哪种更好?」
这个问题,其实就是 Rust 中不可变变量与可变变量的区别。
让我用代码来解释。
第三节:let——恒定灵气
在 Rust 中,用 let 定义的变量,默认是不可变的(immutable)。
fn main() {
let power = 100; // 定义灵气值为 100
println!("当前灵气:{}", power);
}
运行结果:
当前灵气:100
看起来很正常。但如果我们想修改这个值呢?
fn main() {
let power = 100;
power = 200; // 尝试修改灵气值
println!("当前灵气:{}", power);
}
试着运行一下(cargo run),你会看到:
编译器报错了!
别慌,让我们读一下这个错误信息:
显然,可以看到报错信息是我们不能对一个不可变变量二次赋值
编译器还贴心地给了建议:
意识是你可以添加mut关键字,使变量 power 变成可变的,这样你就可以重新对其进行赋值了
这就是 Rust 的「恒定灵气」——一旦定义,就不能改变。
你可能会问:这不是很麻烦吗?为什么要这样设计?
第四节:为什么默认不可变?
这个设计看起来「反直觉」,但其实是 Rust 的核心哲学之一。
让我讲一个故事。
想象你在写一个大型程序,有成百上千行代码。某个变量 user_balance(用户余额)在第 10 行被定义,然后在第 50 行、第 200 行、第 500 行都被使用。
如果这个变量是可变的,那么在这几百行代码的任何地方,它都可能被意外修改。当程序出 bug 时,你需要检查所有这些地方,才能找到问题在哪。
但如果变量默认不可变,那么:
-
你明确知道哪些变量会变,哪些不会变
-
编译器会帮你检查,防止意外修改
-
代码更容易理解,因为你看到一个变量,就知道它的值不会突然变掉
Rust 的哲学:让「可变」成为一个需要明确声明的选择,而不是默认行为。
这就是为什么 Rust 选择「默认不可变」——它让你写出更安全、更可靠的代码。
第五节:let mut——流动灵气
那如果我们确实需要修改变量呢?
很简单,加上 mut 关键字就行了。mut 是 mutable(可变的)的缩写。
fn main() {
let mut power = 100; // 注意这里的 mut
println!("初始灵气:{}", power);
power = 200; // 现在可以修改了
println!("充能后灵气:{}", power);
}
运行结果:
初始灵气:100
充能后灵气:200
这次没有报错。因为我们用 let mut 明确告诉编译器:「这个变量是可变的,我知道我在做什么。」,注意,这是你主动作出的选择
第六节:变量遮蔽——灵气蜕变
除了 mut,Rust 还有另一种「改变」变量的方式,叫做变量遮蔽(Shadowing)。
看这段代码:
fn main() {
let power = 100;
println!("初始灵气:{}", power);
let power = power * 2; // 用 let 重新定义 power
println!("第一次蜕变:{}", power);
let power = power + 50; // 再次重新定义
println!("第二次蜕变:{}", power);
}
运行结果:
初始灵气:100
第一次蜕变:200
第二次蜕变:250
等等,我们不是说 let 定义的变量不能改吗?怎么这里又可以了?
这就是变量遮蔽的妙处:我们没有「修改」原来的变量,而是创建了一个同名的新变量,把旧的「遮住」了。
来看看这张图,是不是更加直观一些?
每次使用let声明这个同名变量就会发生覆盖,上一次的便失效了
第七节:遮蔽 vs mut——有什么区别?
你可能会问:既然变量遮蔽也能「改变」值,那它和 mut 有什么区别?
区别一:遮蔽可以改变类型
fn main() {
let realm = "练气一层"; // 字符串类型
let realm = 1; // 整数类型,没问题!
println!("境界等级:{}", realm);
}
这段代码是合法的。第一个 realm 是字符串,第二个 realm 是整数,它们是完全不同的变量,只是名字相同。
但如果用 mut:
fn main() {
let mut realm = "练气一层";
realm = 1; // ❌ 报错!不能把整数赋给字符串变量
}
mut 允许你改变值,但不能改变类型。
区别二:遮蔽创建新变量,mut 修改原变量
// 遮蔽:每次 let 都创建一个新变量
let power = 100;
let power = 200; // 这是一个新的 power
// mut:始终是同一个变量,只是值在变
let mut power = 100;
power = 200; // 还是同一个 power
什么时候用遮蔽?
场景一:数据转换
fn main() {
let damage = "50"; // 从配置文件读取的伤害值(字符串)
let damage: i32 = damage.parse().unwrap(); // 转成数字才能计算
let final_damage = damage * 2; // 暴击翻倍
println!("最终伤害:{}", final_damage);
}
类型不同,但是其实是同一个概念,没有必要再重新搞一个变量,先前的中间量是一次性的,只用于转换
场景二:不需要旧值了,用同名更清晰
fn main() {
let hp = 100;
let hp = hp + 20; // 升级加血,旧的 hp 不需要了
let hp = hp + 10; // 装备加成
println!("最终生命值:{}", hp);
}
多步计算,方便拆解步骤,只取最后的结果,就像写算式的计算过程,不能一步到位,就先拿去用,工具人属性
遮蔽 vs mut:速查表
| 特性 | let mut | 遮蔽(let) |
|---|---|---|
| 能否改变值 | ✅ 可以 | ✅ 可以 |
| 能否改变类型 | ❌ 不行 | ✅ 可以 |
| 本质 | 修改同一个变量 | 创建新变量,遮住旧的 |
| 适用场景 | 同类型的多次修改 | 类型转换、数据处理 |
一些建议:
-
如果值会频繁改变(比如循环里的计数器),用
mut -
如果是一次性转换(比如字符串转数字),用遮蔽
-
刚开始不用纠结,两种都试试,写多了自然有感觉
第八节:实战:角色属性与战斗系统
理论讲得差不多了,让我们写点真正的代码。
fn main() {
// ══════════════════════════════════════════
// 创建角色
// ══════════════════════════════════════════
let name = "无名侠客"; // 不可变:道号定了就不改
let mut hp = 100; // 可变:战斗中会变
let mut power = 50; // 可变:会消耗和恢复
println!("【创建角色】");
println!(" {} HP:{} 灵气:{}", name, hp, power);
// ══════════════════════════════════════════
// 查阅妖兽图鉴(字符串形式的数据)
// ══════════════════════════════════════════
println!("\n📖 查阅妖兽图鉴...");
let monster_name = "赤焰蛇";
let monster_damage = "35"; // 图鉴上记载的攻击力(字符串)
let monster_drop = "20"; // 图鉴上记载的灵气掉落(字符串)
println!(" {} - 攻击力:{} 掉落灵气:{}",
monster_name, monster_damage, monster_drop);
// ══════════════════════════════════════════
// 进入战斗(字符串转数字,使用遮蔽)
// ══════════════════════════════════════════
println!("\n⚔️ {} 出现了!", monster_name);
// 图鉴数据转为实战数值(遮蔽:同名变量,类型从字符串变成数字)
let monster_damage: i32 = monster_damage.parse().unwrap();
let monster_drop: i32 = monster_drop.parse().unwrap();
// 战斗计算(使用 mut 修改角色属性)
hp = hp - monster_damage;
power = power + monster_drop;
println!(" 受到 {} 点伤害!", monster_damage);
println!(" 击败妖兽,获得 {} 点灵气!", monster_drop);
// ══════════════════════════════════════════
// 战斗结算
// ══════════════════════════════════════════
println!("\n【战斗结束】");
println!(" {} HP:{} 灵气:{}", name, hp, power);
}
【创建角色】
无名侠客 HP:100 灵气:50
📖 查阅妖兽图鉴...
赤焰蛇 - 攻击力:35 掉落灵气:20
⚔️ 赤焰蛇 出现了!
受到 35 点伤害!
击败妖兽,获得 20 点灵气!
【战斗结束】
无名侠客 HP:65 灵气:70
今天我们迈出了修炼的第一步——理解了项目结构,学会了「恒定灵气」与「流动灵气」的区别。
这看起来是个小知识点,但它是 Rust 哲学的基石。理解了「为什么默认不可变」,后面学所有权的时候会顺很多。
下一篇,我们继续探索灵气的不同形态——也就是 Rust 的数据类型。
🔗 关注公众号「小智的代码咖啡屋」,获取系列文章更新一起进步吧~