Rust 修仙录(二):踏入山门——理解你的第一个 Rust 项目

0 阅读10分钟

⏱️ 阅读时长:10 分钟
🎯读完收获:理解 Rust 项目结构 + 掌握 let / let mut / 变量遮蔽

image-20260118082700842

前情提要

上一篇,我们聊了为什么要学 Rust,还在电脑上跑通了第一个程序。

如果你跟着做了,现在你的电脑里应该有一个 rust_demo 文件夹,运行 cargo run 能看到角色信息的输出。

但你可能会有些疑问:

  • cargo new 到底创建了什么?」

  • 「那个 Cargo.toml 文件是干嘛的?」

  • 「为什么代码要写在 src/main.rs 里?」

  • fn main() 又是什么意思?」

今天,我们先把这些问题搞清楚,然后正式开始学习 Rust 的第一个核心概念:变量。

本篇目标

学完这一篇,你将掌握:

✦ Rust 项目的基本结构

Cargo.toml 的作用

fn main() 为什么是程序入口

letlet mut 的区别

✦ 变量遮蔽(shadowing)的用法

我们开始吧。

第一节:认识你的修炼洞府

上一篇我们用 cargo new rust_demo 创建了项目。现在让我们看看这个命令到底创建了什么。

rust_demo/
├── Cargo.toml      ← 项目配置文件
├── src/
│   └── main.rs     ← 你的代码写在这里
└── target/         ← 编译产物(运行后才会出现)

这就是一个最简单的 Rust 项目结构。我喜欢把它比作修仙的「洞府」,这是你平常的工位:

image-20260117223056406

让我们逐个看看这些「设施」。

Cargo.toml:你的门派典籍

打开 Cargo.toml,你会看到类似这样的内容:

[package]
name = "rust_demo"
version = "0.1.0"
edition = "2024"

[dependencies]

这个文件是整个项目的「身份证」和「资源清单」:

字段含义类比
name项目名称你的道号
version版本号你的修为等级
editionRust 版本你修炼的功法版本
[dependencies]依赖的外部库你借用的外门功法

现在 [dependencies] 是空的,因为我们还没用到任何外部库。后面需要时会在这里添加。

你暂时不需要修改这个文件,知道它的作用就行。

src/main.rs:你的修炼室

所有的 Rust 代码都写在 src/ 目录下。对于我们这个简单项目,只有一个文件 main.rs

为什么叫 main.rs

因为 Rust 规定:可执行程序的入口文件必须叫 main.rs。这就像修仙门派规定:「主修炼室必须建在洞府正中央,以便聚集灵气」。

fn main() {
    // 业务代码
}

上一篇我们的代码就写在这里,我们了解下这个部分

部分含义
fn「function」的缩写,表示定义一个函数
main函数的名字,必须叫 main
()参数列表,这里是空的
{ }函数体,你的代码写在这里面

为什么函数必须叫 main

这是 Rust(以及很多其他语言)的约定:程序启动时,操作系统会去找一个叫 main 的函数,然后从那里开始执行。

就像修仙世界的规矩:「功法必须从『吐纳基础』开始。」不是因为这个名字有什么魔力,而是大家都遵守这个约定。

image-20260118070652691

target/:丹药库(编译产物)

当你运行 cargo runcargo build 后,会出现一个 target/ 文件夹。里面存放的是编译后的文件

你通常不需要关心这个文件夹。如果想清理它(比如项目太大了),可以运行:

cargo clean

这里是一些伴随着项目构建产生的文件,生成的可执行文件最后会出现在这里

image-20260118071044442

好,现在你知道代码应该写在哪里、程序是如何运行的了。

接下来,让我们正式开始修炼第一门功法。

第二节:山门考验——你的灵气能改变吗?

image-20260118081218746

你来到山门前,一位守门师兄拦住了你。

> 「想要入门?先回答我一个问题。」

> 「修仙之人,灵气是根本。但灵气有两种——恒定灵气与流动灵气。」

> 「恒定灵气,一旦凝聚,便不可更改,稳如磐石。」

> 「流动灵气,可增可减,灵活多变,但也容易失控。」

> 「你觉得,哪种更好?」

这个问题,其实就是 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),你会看到:

image-20260119081451829

编译器报错了!

别慌,让我们读一下这个错误信息:

显然,可以看到报错信息是我们不能对一个不可变变量二次赋值

编译器还贴心地给了建议:

image-20260119235851129

意识是你可以添加mut关键字,使变量 power 变成可变的,这样你就可以重新对其进行赋值了

image-20260120000705463

这就是 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 明确告诉编译器:「这个变量是可变的,我知道我在做什么。」,注意,这是你主动作出的选择

image-20260120081042828

第六节:变量遮蔽——灵气蜕变

除了 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 定义的变量不能改吗?怎么这里又可以了?

这就是变量遮蔽的妙处:我们没有「修改」原来的变量,而是创建了一个同名的新变量,把旧的「遮住」了。

image-20260121003759243

来看看这张图,是不是更加直观一些?

每次使用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

  • 如果是一次性转换(比如字符串转数字),用遮蔽

  • 刚开始不用纠结,两种都试试,写多了自然有感觉

第八节:实战:角色属性与战斗系统

理论讲得差不多了,让我们写点真正的代码。

image-20260121233218905
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 的数据类型。

image-20260121005425079

🔗 关注公众号「小智的代码咖啡屋」,获取系列文章更新一起进步吧~

image.png