Cargo 基础与编程核心概念串讲

今日教学目标

  1. 掌握 Cargo 项目管理工具的核心命令和工作流。
  2. 理解编程开发中的基本概念:值与类型、函数与方法。
  3. 了解指针与引用的基本概念(为 Day 08-09 所有权学习做铺垫)。
  4. 编写包含多文件结构的 Rust 程序,实践 mod 模块系统。

📌 说明:本天内容严格对应官方 PDF 第 1-3 章(入门指南 + 常见编程概念)。闭包、接口与虚表、泛型、async/await 等高级概念将在对应章节正式讲解,本天仅在”扩展参考”部分做概念预告。


课前小故事:编程语言的”底层逻辑”

你有没有想过,为什么有些人学新语言特别快?因为他们掌握了编程的底层逻辑

无论你用什么语言——C、Java、Python 还是 Rust——底层都绕不开一组核心概念:数据怎么存(值与类型)、代码怎么组织(函数与方法)。

编程语言,不过是这些概念的具体表述和载体。今天我们在 Rust 的语境下,先把最基础的概念讲透。后面学到所有权、生命周期、Trait 等难点时,你会发现它们不过是今天讲的概念的延伸。

📌 今天的内容对应官方 PDF 第 1-3 章。更高级的概念(闭包、泛型、接口、异步等)会在后续对应天数正式讲解。


RustRover 操作指引

创建第 2 天项目

  1. 点击菜单 File → New Project
  2. 选择 Rust,项目名填 day02_concepts,模板选 Binary
  3. 点击 Create

快速验证表达式

写 Rust 时经常想快速验证某个表达式:

1. Scratch File(临时草稿文件)

  • Ctrl+Alt+Shift+Insert
  • 或菜单选择 File → New → Scratch File → Rust
  • 临时文件不会保存到项目中,适合随手试验

2. 悬停查看类型

  • 把鼠标悬停在变量名上
  • RustRover 会弹出提示框,显示变量类型和当前值

3. Evaluate Expression(调试时)

  • 先设置断点(行号左侧点击出现红色圆点)
  • Shift+F9 调试运行
  • 程序停在断点后,Alt+F8 打开求值窗口

常用编辑快捷键

快捷键 作用
Ctrl+Alt+Shift+Insert 新建 Scratch File
Ctrl+B 跳转到定义
Ctrl+/ 注释/取消注释
Ctrl+D 复制当前行
Ctrl+Shift+F10 运行当前文件
Shift+F9 调试运行
Alt+F8 调试时求值表达式
Ctrl+Shift+A 搜索任意动作

常见错误提示

  1. cannot assign twice to immutable variable

    • 原因:对不可变变量重新赋值
    • 解决:加 mut 或用新的 let
  2. mismatched types

    • 原因:变量类型和赋值的类型不一致
    • 解决:检查类型标注
  3. expected type, found {integer}

    • 原因:整数字面量类型不明确
    • 解决:加上类型标注,如 let x: u32 = 5;

语法讲解

💡 初学者提示:这一节概念较多,如果一遍看不懂没关系。后面的 Day 3-4 会用具体代码再次演示这些概念。先把”值有类型、类型决定大小”这个核心记住。

一、Cargo 基础深入

Cargo 是 Rust 的”瑞士军刀”,身兼数职。它类似 Node.js 的 npm、Golang 的 go,用来做依赖管理以及开发过程中的任务管理。

Cargo 项目结构

1
2
3
4
5
6
day02_concepts/
├── Cargo.toml ← 项目配置文件(TOML 格式)
├── Cargo.lock ← 依赖版本锁定文件(自动生成)
├── src/
│ └── main.rs ← 程序入口
└── target/ ← 编译产物

Cargo.toml 详解

1
2
3
4
5
6
7
8
9
10
11
[package]
name = "day02_concepts" # 包名
version = "0.1.0" # 语义化版本号
edition = "2021" # Rust 语言版本
authors = ["Your Name"] # 作者信息

[dependencies]
# 第三方依赖写在这里
# 例如:
# serde = "1.0" # 指定版本
# reqwest = { version = "0.12", features = ["json"] } # 带特性的依赖

💡 类比理解Cargo.toml 就像一份餐厅的菜单和营业执照。菜单(dependencies)告诉顾客能吃到什么菜,营业执照(package)说明你叫什么名字、是什么版本。一个 Rust 项目也被称为一个 crate

Rust 项目的组织结构

当 Rust 代码规模越来越大时,可以用 mod 来组织代码。

  • crate:一个项目就是一个 crate,可以是可执行项目,也可以是库(用 cargo new --lib 创建)
  • mod:用 mod 在入口文件中声明要加载的其它代码文件
  • workspace:当代码规模继续增长,一个 workspace 可以包含多个 crates,只有涉及的 crate 需要重新编译

单元测试与集成测试

1
2
3
4
5
6
7
8
9
// 单元测试:和被测代码放在同一个文件中
// 使用条件编译 #[cfg(test)] 确保只在测试时编译
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4); // 断言相等
}
}
  • 单元测试:放在和被测代码相同的文件中,使用 #[cfg(test)] 条件编译
  • 集成测试:放在 tests 目录下(与 src 平行),只能测试 crate 的公开接口
  • 运行测试:cargo test

二、值与类型

类型是对值的区分

严谨地说,类型是对值的区分,它包含了值在内存中的长度对齐以及值可以进行的操作等信息。一个是符合某个特定类型的数据实体。

比如 64u8,它是 u8 类型,对应一个字节大小、取值范围在 0~255 的某个整数实体,这个实体是 64。

⚠️ 关键理解:值无法脱离具体的类型讨论。同样是内存中的一个字节 0x40,如果其类型是 u8,那么含义是 64;如果其类型是 ASCII char,那么含义是 @ 符号。

原生类型与组合类型

  • 原生类型:编程语言提供的最基础的数据类型。所有原生类型的大小都是固定的,因此它们可以被分配到栈上。
  • 结构体:多个类型组合在一起共同表达一个值的复杂数据结构。比如 User 结构体包含 name、age、email。
  • 标签联合:可以存储一组不同但固定的类型中的某个类型的对象,具体是哪个类型由其标签决定。Rust 中的 enum 就是标签联合。

Rust 中的基本数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fn main() {
// ===== 标量类型 =====
let a: i32 = 42; // 32位有符号整数,最常用
let b: u32 = 100; // 32位无符号整数
let c: i64 = -1_000_000; // 64位有符号整数,下划线分隔提高可读性
let d: u8 = 255; // 8位无符号整数,最大255

let pi: f64 = 3.14159; // 64位浮点数,默认推荐
let e: f32 = 2.718; // 32位浮点数

let is_fun: bool = true; // 布尔值
let letter: char = 'A'; // char 占 4 字节,支持 Unicode
let emoji: char = '🦀'; // Rust 吉祥物 Ferris

// ===== 复合类型 =====
// 元组:不同类型的值打包在一起
let tup: (i32, f64, &str) = (1, 2.0, "hi");
let (x, y, z) = tup; // 解构
let first = tup.0; // 索引访问

// 数组:相同类型,长度固定
let arr = [1, 2, 3, 4, 5];
let nums: [i32; 5] = [10, 20, 30, 40, 50];
let zeros = [0; 3]; // [0, 0, 0]

println!("整数: {}, 浮点: {}, 布尔: {}, 字符: {}", a, pi, is_fun, emoji);
println!("元组:({}, {}, {}), 数组第一个: {}", x, y, z, arr[0]);
}

三、指针与引用

在内存中,一个值被存储到某个位置,这个位置对应一个内存地址。

  • 指针:一个持有内存地址的值,可以通过解引用(dereference)来访问它指向的内存地址,理论上可以解引用到任意数据类型。
  • 引用:和指针类似,但解引用访问是受限的,只能解引用到它引用数据的类型,不能用作它用。

⚠️ 安全提示:指针的使用限制更少,但也会带来更多危害。如果没有用正确的类型解引用一个指针,会引发内存问题。引用更安全,Rust 大量使用引用而非裸指针。

胖指针(Fat Pointer)

某些引用除了需要指针指向内存地址外,还需要额外的元数据。比如指向字符串切片的引用 &str 是「指针 + 长度」共 2 个 word(64 位下 16 字节);而 String 是「指针 + 长度 + 容量」共 3 个 word(24 字节)。这种比普通指针携带更多信息的指针,称为胖指针。

四、语句与表达式

Rust 新手最容易混淆的概念之一:

  • 语句(Statement):执行一个动作,没有返回值,以分号 ; 结尾
  • 表达式(Expression):计算并产生一个值,不以分号结尾
1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
// 语句
let x = 5;

// 用表达式赋值
let y = {
let a = 1;
let b = 2;
a + b // 注意没有分号!整个 {} 块是表达式,值为 3
};

println!("y = {}", y); // 3
}

💡 类比理解:语句就像给厨师下的命令——“切洋葱”、”开火”;表达式就像一道菜——厨师做完后端给你(返回一个值)。函数最后一行如果是表达式,就是把这道菜端给调用者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 返回值是表达式 vs 隐式返回 unit
fn pi() -> f64 {
3.1415926 // 没有分号,这是表达式,返回 f64
}

fn not_pi() {
3.1415926; // 有分号,这是语句,返回 unit(空元组)
}

fn main() {
let is_pi = pi(); // f64
let is_unit = not_pi(); // ()
println!("is_pi: {:?}, is_unit: {:?}", is_pi, is_unit);
// 输出:is_pi: 3.1415926, is_unit: ()
}

方法

在面向对象的编程语言中,在类或对象中定义的函数被称为方法。方法往往和对象的引用发生关系,比如 Python 对象的 self,或 Java 对象的 this


📌 扩展参考(超出本天范围,后续正式讲解)

以下内容为概念预告,帮助你在后续学习中有心理准备。不需要现在就掌握,了解概念即可。正式讲解和练习在对应的天数。

五、闭包(详见 Day 19)

闭包是将函数、或者说代码和其环境一起存储的一种数据结构。闭包引用的上下文中的自由变量,会被捕获到闭包的结构中,成为闭包类型的一部分

1
2
3
4
5
6
7
8
9
fn main() {
let multiplier = 3; // 自由变量

// 闭包捕获了 multiplier
// |参数| -> 返回类型 { 函数体 }
let multiply = |x: i32| x * multiplier;

println!("3 * 5 = {}", multiply(5)); // 15
}

💡 类比理解:函数就像一份打印好的菜谱,谁都能照着做;闭包就像一份装了”秘密调料”的菜谱——调料(自由变量)已经打包在菜谱里了,不管谁拿到这份菜谱,用的都是同一份调料。


六、接口与虚表(详见 Day 14/17/18)

📌 概念预告:接口是软件设计的核心抽象层。Rust 用 trait 来实现接口概念。虚表是实现运行时多态的底层机制。这些内容将在 Day 14(模块系统 + trait 初识)、Day 17(泛型与 trait 深入)、Day 18(Trait Object 与动态分发)中正式讲解。

接口是一个软件系统开发的核心部分,它反映了系统设计者对系统的抽象理解。作为一个抽象层,接口将使用方和实现方隔离开来,使两者不直接有依赖关系,大大提高了复用性和扩展性。

很多编程语言都有接口的概念:Java 的 interface、Swift 的 protocol、Rust 的 trait

虚表(Virtual Table)

当我们在运行期使用接口来引用具体类型的时候,变量原本的类型被抹去。因此需要构建胖指针,除了指向数据本身外,还需要指向一张涵盖了这个接口所支持方法的列表——这就是虚表。

七、并发与异步(详见 Day 26-28)

📌 概念预告:async/await 是 Rust 处理异步 I/O 的核心语法。这些内容将在 Day 26(并发处理)、Day 27(Future 与 async/await)、Day 28(异步 IO 与网络开发)中正式讲解。

操作系统为应用程序提供了异步 I/O,让应用可以在当前 I/O 处理完毕之前,将 CPU 时间用作其它任务的处理。

Promise / async / await

异步处理完成后的结果,一般用 Promise(也叫 Future/Delay/Deferred)来保存。它代表在未来的某个时刻才能获得的结果的值,存在三个状态:

  1. 初始状态:Promise 还未运行
  2. 等待(pending)状态:Promise 已运行,但还未结束
  3. 结束状态:成功解析出一个值,或者执行失败

async 定义了一个可以并发执行的任务,await 触发这个任务并发执行。大多数语言中,async/await 是一个语法糖,它使用状态机将 Promise 包装起来,让异步调用的使用感觉和同步调用类似。


八、泛型编程(详见 Day 17)

📌 概念预告:泛型是 Rust 实现代码复用的核心机制。将在 Day 17(泛型与 trait 深入)中正式讲解。

数据结构的泛型

泛型编程包含两个层面。首先是数据结构的泛型(参数化类型),比如:

1
2
3
4
5
// 这是一个泛型结构体,S 是类型参数
struct Connection<S> {
io: S,
state: State,
}

参数 S 的具体类型只有在使用 Connection 的上下文中才得到绑定。你可以把参数化数据结构理解成一个产生类型的函数——“调用”时接受具体类型参数,返回携带这些类型的类型。

💡 类比理解:泛型就像一个”万能模具”。你造了一个 Connection<S> 的模具,当你需要 TcpStream 类型的连接时,就把 S 替换成 TcpStream,得到 Connection<TcpStream>

但问题来了:如果 S 可以是任意类型,怎么知道 S 有什么行为?这就需要用接口对 S 进行约束。所以支持泛型编程的语言,必然提供强大的接口编程能力。

代码的泛型化

泛型编程的另一个层面是使用泛型结构后代码的泛型化。比如二分查找,用 C 写只能针对 int[],换一种数据类型就要重写;而用 C++ 的模板/迭代器,同一套代码可以适用于多种数据类型。

泛型编程带来的好处是延迟绑定,让数据结构通用性更强;也大大减少代码重复,提高可维护性。


常见错误 / 避坑指南

1. 混淆语句和表达式

1
2
3
4
5
6
7
8
9
// 错误:最后加了分号,变成语句,返回 unit 而非 i32
fn wrong() -> i32 {
5; // 编译错误:expected i32, found ()
}

// 正确:去掉分号
fn right() -> i32 {
5
}

2. 函数参数类型不明确

1
2
3
4
5
// 错误:Rust 无法推断 x 的类型
fn main() {
let x = something;
// 需要标注:let x: i32 = 5;
}

3. 混淆并发和并行

记住:并发是”同时处理多件事的能力”(CPU 时间片轮转),并行是”同时做很多件事”(多核 CPU 同时执行)。

💡 类比理解:并发就像一个厨师同时做几道菜——先切菜,趁炖汤的时候去炒另一个菜;并行就像多个厨师同时做不同的菜。


🔒 安全视角延伸

依赖安全:你的供应链就是你的攻击面

Rust 的 Cargo 生态系统让引入第三方库变得异常简单——一行 Cargo.toml 就能添加一个 crate。但便利的背后隐藏着一个严肃的安全问题:你依赖的每一个 crate 都是潜在攻击面

为什么依赖是安全隐患?

一个典型的 Rust 项目可能直接依赖 10-20 个 crate,但加上间接依赖(依赖的依赖),总数可能高达数百个。这些 crate 中任何一个如果包含漏洞或恶意代码,都可能被攻击者利用。这就是所谓的供应链攻击(Supply Chain Attack)

例如,2024 年安全研究人员发现多个恶意 npm/PyPI 包仿冒流行库名称进行钓鱼。Rust 生态虽然相对年轻,但并非免疫——crate 注册表是开放的,任何人都可以发布包。

cargo-audit:扫描已知漏洞

Rust 社区提供了 cargo-audit 工具,它会对你的 Cargo.lock 进行检查,比对 RustSec Advisory Database 中已公开的漏洞数据库:

1
2
3
4
5
# 安装 cargo-audit 工具
cargo install cargo-audit

# 在项目根目录运行审计
cargo audit

运行后,cargo audit 会输出类似下面的报告:

1
2
3
4
5
6
7
8
9
10
11
12
13
CARGO_AUDIT_REPORT

Title: RUSTSEC-2020-0036
Package: chrono
Version: 0.4.19
Date: 2020-05-19
URL: https://rustsec.org/advisories/RUSTSEC-2020-0036
Severity: 0.7 (medium)
Description:
chrono may panic on formatted negative dates
Affected versions of chrono may panic on formatted negative dates.

The fix is in chrono 0.4.20+.

报告会告诉你哪个 crate 的哪个版本存在漏洞、严重程度、修复版本。你只需在 Cargo.toml 中升级到安全版本即可。

Cargo.lock:锁定版本的重要性

Cargo.lock 文件记录了每个依赖的确切版本号。不要随意执行 cargo update——它可能会把某个 crate 升级到一个你尚未审查的新版本。在团队协作中,Cargo.lock 应该被提交到版本控制,确保所有成员使用完全相同的依赖版本。

💡 类比理解Cargo.lock 就像餐厅的采购清单——上面写明了每个供应商的批次号。如果某个批次出了问题,你可以精确地追溯和替换。没有这份清单,你每次买到的食材可能都不一样,出了问题很难排查。

cargo-deny:许可证与来源审查

除了漏洞,还需要关注依赖的许可证合规性来源可信度cargo-deny 可以检查:

1
2
3
4
5
6
7
8
# 安装 cargo-deny
cargo install cargo-deny

# 初始化配置文件
cargo deny init

# 运行检查(漏洞 + 许可证 + 来源)
cargo deny check

配置文件 deny.toml 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 💡 unsafe 是 Rust 的"逃生舱",允许绕过安全检查。将在 Day 25 详细学习。
# deny.toml — cargo-deny 配置文件

[advisories]
# 检查已知漏洞
db-urls = ["https://github.com/rustsec/advisory-db"]

[licenses]
# 允许的许可证类型
allow = [
"MIT", # 最宽松的开源许可证之一
"Apache-2.0", # Apache 2.0 许可证
"BSD-3-Clause", # BSD 三条款许可证
"ISC", # ISC 许可证
]

[bans]
# 禁止使用的 crate(可自定义)
deny = [
# 例如禁止某个已知不安全的 crate
# { name = "unsafe-lib", version = "*" }
]

[sources]
# 只允许从 crates.io 拉取依赖,禁止未知来源
unknown-registry = "deny"
unknown-git = "deny"

安全实践清单

  1. 定期运行 cargo audit:建议集成到 CI/CD 流水线中
  2. 提交 Cargo.lock:确保团队依赖版本一致
  3. 谨慎 cargo update:升级前查看 changelog
  4. 使用 cargo deny:检查许可证和来源
  5. 审查新依赖:添加新 crate 前查看其维护状态和下载量

📖 延伸阅读RustSec Advisory Database — Rust 生态系统的安全公告数据库,可以订阅 RSS 获取最新漏洞通告。


课后实验作业

作业 1:函数作为一等公民

  • 编写函数 apply(value: i32, f: fn(i32) -> i32) -> i32
  • 实现 squarecube 两个函数传入 apply 测试
  • println! 打印结果

作业 2:语句与表达式练习

  • 编写函数,分别返回 f64()(unit),体会表达式和语句的区别
  • 使用代码块 {} 作为表达式赋值给变量

作业 3:模块组织练习

  • 创建一个新项目,在 src/main.rs 中使用 mod 引入一个 math_utils 模块
  • math_utils.rs 中定义 addmultiply 函数
  • main 中调用这两个函数

作业 4(大实验种子):Agent 配置常量

  • pi_agentmain.rs 中定义常量:
    1
    2
    3
    const AGENT_NAME: &str = "Pi Agent";
    const DEFAULT_TEMPERATURE: f64 = 0.7;
    const MAX_HISTORY: usize = 10;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- 在 `main` 中打印这些常量

---

## 参考答案

### 作业 1 答案

​```rust
// day02_concepts/src/main.rs

// 接受一个函数作为参数
fn apply(value: i32, f: fn(i32) -> i32) -> i32 {
f(value) // 调用传入的函数
}

// 平方函数
fn square(value: i32) -> i32 {
value * value
}

// 立方函数
fn cube(value: i32) -> i32 {
value * value * value
}

fn main() {
// 函数作为参数传递
println!("apply square(2): {}", apply(2, square)); // 4
println!("apply cube(2): {}", apply(2, cube)); // 8

// 也可以传入闭包
let double = |x| x * 2;
println!("apply double(5): {}", apply(5, double)); // 10
}

作业 2 答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// day02_concepts/src/main.rs

// 返回 f64 的函数——最后一行是表达式(无分号)
fn pi() -> f64 {
3.1415926
}

// 返回 unit 的函数——最后一行是语句(有分号)
fn not_pi() {
3.1415926; // 分号让它变成语句
}

fn main() {
let is_pi = pi();
let is_unit = not_pi();

println!("is_pi: {:?}, is_unit: {:?}", is_pi, is_unit);
// 输出:is_pi: 3.1415926, is_unit: ()

// 代码块作为表达式
let result = {
let a = 10;
let b = 20;
a + b // 没有分号,整个块的值是 30
};
println!("代码块结果: {}", result); // 30

// 如果加了分号
let result2 = {
let a = 10;
let b = 20;
a + b; // 有分号,块的值是 ()
};
println!("带分号结果: {:?}", result2); // ()
}

作业 3 答案

1
2
3
4
5
6
7
8
9
10
11
// day02_concepts/src/math_utils.rs

// 加法函数
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

// 乘法函数
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// day02_concepts/src/main.rs

// 声明模块
mod math_utils;

fn main() {
// 使用模块中的函数
let sum = math_utils::add(3, 5);
let product = math_utils::multiply(4, 6);

println!("3 + 5 = {}", sum);
println!("4 * 6 = {}", product);
}

作业 4 答案(大实验种子)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// pi_agent/src/main.rs

// Agent 名称常量
const AGENT_NAME: &str = "Pi Agent";
// 默认温度参数,控制 AI 输出的随机性
const DEFAULT_TEMPERATURE: f64 = 0.7;
// 最大历史消息条数
const MAX_HISTORY: usize = 10;

fn main() {
println!("Pi Agent initialized.");

// 打印配置常量
println!("Agent 名称: {}", AGENT_NAME);
println!("默认温度: {}", DEFAULT_TEMPERATURE);
println!("最大历史: {}", MAX_HISTORY);
}

今日小结

  • Cargo 是 Rust 的项目管理工具,类似 npm/go,管理依赖和构建流程。
  • 值无法脱离类型讨论;类型分为原生类型(固定大小、栈上分配)和组合类型(结构体、标签联合)。
  • 指针持有内存地址,引用是受限的指针——Rust 优先使用引用保证安全。
  • 胖指针除了地址还携带长度等信息,Rust 中很多数据结构引用都是胖指针。
  • 函数是一等公民,可以作为参数和返回值。(闭包将在 Day 19 正式讲解)
  • 语句执行动作返回 (),表达式产生值——Rust 绝大多数代码都是表达式。
  • 接口隔离使用方和实现方,虚表是运行时多态的基础。
  • 并发是能力,并行是手段;同步阻塞,异步不阻塞。
  • 泛型编程让数据结构像函数一样延迟绑定类型参数,提升通用性。
  • 今天我们给 pi_agent 添加了配置常量,明天深入内存的堆与栈。