第 6 课:结构体、枚举与方法 ------ 开始定义自己的类型
预计时间:75-90 分钟 | 难度:⭐⭐ | 前置:第 4-5 课(所有权与生命周期)
6.1 这节课解决什么问题
前几课一直在用语言内置的类型(数字、字符串、数组)。从本课起,你要学会为自己的业务造类型------课程实战里的"用户、会话、消息、API 请求/响应"全都得靠本课知识建模。
本课会掌握四件武器:
struct 结构体 ------ 把相关字段打包(类似 JS 对象 / Python class 的"字段集合")
enum 枚举 ------ 表示"是 A、B 或 C"的互斥状态(这是 Rust 比大多数语言强的地方)
impl 方法 ------ 给类型挂行为(相当于"类的方法")
match 匹配 ------ 拆开并穷尽地处理 enum 的所有可能
另外你会认识 Rust 里出镜率最高的两个内置枚举 Option<T> 和 Result<T, E>------理解它们,等于理解半个 Rust 错误处理体系。
6.2 结构体 struct:打包相关数据
6.2.1 定义、构造与访问
rust
// 字段命名惯例:snake_case
struct User {
username: String, // 拥有自己的数据(不是引用,见 5.6 的建议)
email: String,
age: u8,
active: bool,
}
fn main() {
// 构造:字段顺序无所谓,全部字段都要给值
let mut user = User {
username: String::from("xiaoling"),
email: String::from("xl@example.com"),
age: 26,
active: true,
};
// 访问/修改(修改要求实例是 mut)
println!("{}", user.username);
user.age = 27;
// 取字段的引用(避免 move 走字段),后面调用只读函数常用
let name_ref: &String = &user.username;
println!("{name_ref}");
}
💡 注意
username: String是拥有型 字段:当整个user被 move 或 Drop 时,内部的String一并被释放------所有权在这里一层层组合,编译器全自动管好。
6.2.2 字段初始化简写与 .. 更新语法
rust
struct User {
username: String,
email: String,
age: u8,
active: bool,
}
fn build_user(username: String, email: String) -> User {
// 字段名与变量名相同 → 简写(去掉 username: username 的重复)
User {
username,
email,
age: 0,
active: true,
}
}
fn main() {
let u1 = build_user(String::from("a"), String::from("a@x.com"));
// ..u1:其余字段从 u1 "复制/移动"过来(u1.username 等被 move → u1 不可整体再用)
let u2 = User {
username: String::from("b"),
..u1 // 等价于手动填 email: u1.email, age: u1.age, active: u1.active
};
println!("{} {}", u2.email, u2.age);
// println!("{}", u1.active); // 可以单独用 u1 没被移动的 Copy 字段
// println!("{}", u1.username); // ❌ 已被 move 进 u2
}
⚠️
..u1会把没显式写出的非 Copy 字段 move 走 ,所以u1整体就不能再用了(部分字段还可以)。若想保留u1,先u1.clone()再更新。
6.2.3 三种结构体形态
rust
// ① 普通结构体:有名字的字段
struct Rectangle {
width: u32,
height: u32,
}
// ② 元组结构体:字段没名字,靠位置区分(用于"包装一个值+类型区分")
struct Color(u8, u8, u8);
struct Point(i32, i32);
// ③ 单元结构体:没有字段(用于实现 trait / 标记,第 8 课会遇到)
struct Marker;
fn main() {
let rect = Rectangle { width: 10, height: 20 };
let black = Color(0, 0, 0);
let origin = Point(0, 0);
let _m = Marker;
println!("{}x{}", rect.width, rect.height);
println!("RGB=({},{},{})", black.0, black.1, black.2); // 按序号访问
println!("origin: ({}, {})", origin.0, origin.1);
}
6.3 方法 impl:给类型挂行为
6.3.1 方法与三种接收者
方法与函数区别只有一点:方法在 impl 块里,且第一个参数是 self(自身实例)。
rust
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// 关联函数:不接收 self(类似静态方法 / User::new)。用 :: 调用
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
// 方法一:&self ------ 只读借用(最常见)
fn area(&self) -> u32 {
self.width * self.height
}
// 方法二:&mut self ------ 修改自己
fn double_width(&mut self) {
self.width *= 2;
}
// 方法三:self ------ 拿走自己(消费 self,之后实例不可用)
fn into_string(self) -> String {
format!("{}x{}", self.width, self.height)
}
}
fn main() {
let mut r = Rectangle::new(3, 4); // 关联函数 ::new
println!("area = {}", r.area()); // 方法调用 r.method()
r.double_width();
println!("now {}x{}", r.width, r.height);
let desc = r.into_string(); // r 被消费
// println!("{r:?}"); // ❌ r 已被 move 进方法
println!("{desc}");
}
三种接收者的选择口诀:
| 接收者 | 借用性质 | 场景 | 之后还能用实例吗 |
|---|---|---|---|
&self |
只读借用 | 查询/计算 | ✅ 能用 |
&mut self |
可变借用 | 修改自身字段 | ✅ 能用 |
self |
拿走所有权 | 消费自身、返回别的(into_xxx) |
❌ 不能再碰 |
💡
&self是self: &Self的语法糖,Self指"当前这个类型"。别被各种写法吓到,方法里写的self就是"当前实例"。
6.3.2 同名方法名 + 字段名(Rust 允许)
rust
struct Circle { radius: f64 }
impl Circle {
fn radius(&self) -> f64 { self.radius } // 方法叫 radius,字段也叫 radius,合法
}
fn main() {
let c = Circle { radius: 1.5 };
println!("{}", c.radius()); // 带括号=方法,不带=字段:c.radius
}
6.3.3 给枚举也挂 impl
方法不只属于 struct,enum 同样可以(下一节定义完 enum 立刻举例)。
6.4 枚举 enum:表达"是这几种之一"
6.4.1 无数据变体(像"状态列表")
rust
enum Platform {
Ios,
Android,
Desktop,
}
impl Platform {
fn label(&self) -> &'static str {
match self {
Platform::Ios => "iOS",
Platform::Android => "Android",
Platform::Desktop => "桌面",
}
}
}
fn main() {
let p = Platform::Ios;
println!("{}", p.label());
}
6.4.2 带数据变体(Rust 的杀手锏)
这是其他语言里 enum 少见的能力------每个变体可以携带不同类型的数据:
rust
enum Message {
Quit, // 无数据
Move { x: i32, y: i32 }, // 带"命名数据"(像内联结构体)
Write(String), // 带一个 String
Send { to: String, body: String }, // 带多个
}
fn main() {
let m1 = Message::Quit;
let m2 = Message::Move { x: 1, y: 2 };
let m3 = Message::Write(String::from("hi"));
// 用 match 拆开处理(穷尽性检查会逼你覆盖每个变体)
match m2 {
Message::Quit => println!("退出"),
Message::Move { x, y } => println!("移动到 ({x}, {y})"),
Message::Write(text) => println!("写入:{text}"),
Message::Send { to, body } => println!("发给 {to}:{body}"),
}
}
用途直击:一个"命令"可以是"登录 / 发送消息 / 切换会话 / 清空历史"里的某一种,每种携带不同参数------第 17 课实战的 UI→Rust 命令通道,就会用带数据 enum 建模命令。
6.4.3 Option<T>:可能"没有值"的类型
Rust 没有 null,用枚举表达"可能为空":
rust
enum Option<T> {
None, // 没有值
Some(T), // 有值,类型 T
}
rust
fn main() {
let some_num: Option<i32> = Some(5);
let no_num: Option<i32> = None;
// Option 不能直接当 i32 用!这正是"消灭空指针"的关键:
// let sum = some_num + 1; // ❌ error[E0369]: cannot add `i32` to `Option<i32>`
// 编译器逼你先处理"可能没有值"的情况
// 解包方式一:match 穷尽处理
let v = match some_num {
Some(n) => n,
None => 0, // 不写这个分支 → 编译报错(穷尽性)
};
println!("{v}");
}
处理 Option 的常用 API(全在标准库,先混脸熟):
rust
fn main() {
let o: Option<i32> = Some(10);
println!("{:?}", o.is_some()); // true
println!("{:?}", o.unwrap_or(0)); // 有值取 10,None 时给默认 0
println!("{:?}", o.map(|n| n * 2)); // 把 Some(10) 变 Some(20)(闭包见 9 课)
println!("{:?}", o.ok_or("没有值")); // 转成 Result(见 6.4.4)
// unwrap / expect:坚信有值才用,否则 panic
let n = o.expect("o 竟然为 None?");
println!("{n}");
}
⚠️
unwrap/expect在 None 时会 panic。临时练习随便用;真实代码里要在能证明"这里不可能为 None"时才用 (如刚 insert 完的HashMap::get二次查询)。7 课会给出更系统的处理策略。
6.4.4 Result<T, E>:可能成功或失败
rust
enum Result<T, E> {
Ok(T), // 成功,携带值 T
Err(E), // 失败,携带错误 E
}
rust
fn main() {
let parsed: Result<i32, _> = "42".parse(); // 字符串→数字:成功是 Ok
let failed: Result<i32, _> = "abc".parse(); // 失败是 Err
let r = match parsed {
Ok(n) => format!("解析成功:{n}"),
Err(e) => format!("解析失败:{e}"),
};
println!("{r}");
// Err 分支不写?穷尽性检查报错(Result 必须两个分支都覆盖或用通配)
let r2 = match failed {
Ok(n) => n,
Err(_) => -1, // 通配 _ 忽略错误内容
};
println!("{r2}");
}
💡
Option(有无)与Result(成败)是 Rust 两个"官方钦定"的核心枚举。它们如此常用以至于有专门语法糖(?运算符、if let、.ok_or()),第 7 课错误处理整课都是围绕Result展开。
6.5 match 深入:穷尽性、通配、绑定与守卫
6.5.1 穷尽性(exhaustiveness)
match 必须覆盖枚举所有 变体,否则编译不过。这是它作为"安全开关"的根基------以后给枚举加新变体(比如给命令加一种"清空历史"),所有 match 它的地方全都会编译报错,逼你逐个补齐处理。这在实战里是极好的"改一处提醒全流程"机制。
6.5.2 通配 _ 与绑定 @、守卫 if
rust
fn main() {
let x = Some(42);
// 通配:不关心的值用 _ 或变量名兜底
match x {
Some(0) => println!("恰好是 0"),
Some(_) => println!("是某个非零值"), // _ 忽略内部值
None => println!("没有值"),
}
// 匹配同时绑定部分内容:@
match x {
Some(n @ 1..=10) => println!("1 到 10 之间:{n}"),
Some(n @ 40..=50) => println!("40 到 50 之间:{n}"),
Some(_) => println!("其他数"),
None => println!("None"),
}
// 守卫 if:分支条件更复杂时用(守卫不参与穷尽性判断)
let score = 66;
match score {
s if s >= 90 => println!("优秀"),
s if s >= 60 => println!("及格"),
_ => println!("加油"),
}
}
6.5.3 整数匹配的"穷尽"其实是范围
对数字/布尔 match,穷尽性要求覆盖所有可能值,所以一般要么用 1..=10 区间、要么用 _ 兜底。
6.6 if let / while let:只关心一种情况时的简写
当 match 只关心一个变体、其他全忽略时,可以简写:
rust
fn main() {
let config: Option<&str> = Some("release");
// match 写法
match config {
Some(v) => println!("模式:{v}"),
None => (),
}
// if let 简写(更常用、更贴近意图)
if let Some(v) = config {
println!("模式:{v}");
} else {
println!("未配置");
}
// while let:不断尝试直到不是这个变体
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
print!("{top} "); // 3 2 1
}
println!();
}
💡 判断标准:只关心一种情况 + 其余忽略 →
if let;需要覆盖多种并各自处理 →match。 两者不是竞争关系,是详略不同的同一件事。
6.7 为 struct 打印调试:#[derive(Debug)]
前几课我们用 {:?} 打印过数组,但自定义 struct 默认不支持 {:?}:
rust
#[derive(Debug)] // 派生宏:自动实现 Debug,让 {:?} 能打印
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("{p:?}"); // Point { x: 1, y: 2 }
println!("{p:#?}"); // 多行美化版
}
💡
#[derive(...)]可以一次列多个:#[derive(Debug, Clone, PartialEq)]。8 课会给一张派生宏速查表;实战建模时每定义一个结构体都顺手#[derive(Debug, Clone, PartialEq)],这是 Rust 工程里的日常习惯。
6.8 读报错专项:Option 相关的常见错误
① 把 Option 直接当值算
let x: Option<i32> = Some(5);
x + 1; // ❌ error[E0369]: cannot add `{integer}` to `Option<i32>`
修法:先解包(match / if let / unwrap_or / ?)。
② match 忘记 None 分支
error[E0004]: non-exhaustive patterns: `None` not covered
修法:补 None => ... 或用 if let、unwrap_or。
③ unwrap 在 None 上 panic
thread 'main' panicked at ...: called `Option::unwrap()` on a `None` value
这是运行期错误(不是编译错误)------练习阶段正常,真实代码请先想清楚会不会 None。
6.9 📝 动手练习
参考实现放 code/06-struct-enum/(写作时同步给出)。本章主题是建模,练习以"用户/订单/会话"为素材,为实战篇热身:
- 用户建模 :定义
struct User { id: u32, name: String, email: String, vip: bool },写关联函数new与fn display(&self) -> String;构造 3 个用户放入Vec<User>并循环打印(只读遍历用for u in &users)。 - 订单状态机 :定义
enum OrderStatus { Pending, Paid(u32 /*金额*/), Shipped { carrier: String, tracking_no: String }, Cancelled(String) },写fn describe(&self) -> String用 match 描述;测试每种状态。 - Rectangle 方法集 :给 6.2.3 的 Rectangle 补方法
can_hold(&self, other: &Rectangle) -> bool、scale(&mut self, k: u32)、to_string_own(self) -> String,并在 main 中分别用 &、&mut、self 接收者调用。 - Option 转换练习 :写
fn safe_div(a: i32, b: i32) -> Option<i32>(除零返回 None);再写fn find_user<'a>(users: &'a [User], id: u32) -> Option<&'a User>(遍历找 id,没有返回 None)------体会返回引用时生命周期如何被自动推断。 - if let 训练 :给定
let config = Some(("host", 8080));,用if let打印主机与端口,用while let把vec![1,2,3]pop 干净。 - 消息系统小建模(综合) :用带数据 enum 实现
Message(如 6.4.2),写fn handle(msg: Message) -> String,用 match + 守卫对"内容过长"(>20 字符)做专门处理,其余正常返回。再写测试数据跑通。
验收门禁 :能默写 struct 三种形态与 impl 三种接收者;能解释 enum 带数据变体的价值;能说清 Option/Result 的定义(Some(T)/None、Ok(T)/Err(E))与穷尽性检查为什么是 Rust 的"安全开关"。
✅ 本节小结
- struct :普通/元组/单元三种形态;字段简写与
..更新语法;字段默认不实现 Debug,用#[derive(Debug)]; - impl :关联函数
Type::new()(无 self)、方法三兄弟&self/&mut self/self,按"只读/修改/消费"选接收者; - enum :可携带不同类型数据的变体,是建模"互斥状态"的最强工具;
Option<T>(有无)与Result<T,E>(成败)是两个核心内置枚举; - match :穷尽性逼你覆盖所有分支(新增变体=编译期提醒全链路);
_通配、@绑定、if守卫;if let/while let用于只关心单变体; - 经验法则 :领域类型用拥有的
String/Vec而非引用;能#[derive]就 derive,别手写样板。
下一课预告 :第 7 课《错误处理:panic、Result 与 ?》------Rust 没有异常(没有 try/catch),它如何优雅处理"会失败的事"?panic! 的两类错误、? 运算符如何把错误一层层往上抛、以及用 thiserror/anyhow 管理真实项目的错误类型。这是做网络/文件/JSON 解析(13-15 课)前必备的一块拼图。