结构体、枚举与模式匹配 —— 给数据穿上合身的衣服
在真实的编程世界里,数据很少是孤零零的一个数字或一段文字。一个用户有姓名、邮箱、年龄;一个订单有商品、数量、状态。把这些相关的数据组织在一起,就需要”自定义类型”。Rust用结构体(struct)和枚举(enum)来帮你给数据穿上量身定做的衣服。
📋 开篇自测:你已经知道多少?
- 你用过其他语言的class或struct吗?Rust的struct和它们有什么异同?
- 你知道”代数数据类型”(ADT)这个概念吗?Rust的enum有多强大?
- 模式匹配(pattern matching)和传统的switch-case有什么本质区别?
一、结构体(Struct)——数据的收纳盒
1.1 定义和实例化
结构体就像一个带标签的收纳盒——每个格子有自己的名字和用途:
struct Player {
username: String,
level: u32,
health: f64,
is_online: bool,
}
fn main() {
let player1 = Player {
username: String::from("DragonSlayer"),
level: 42,
health: 98.5,
is_online: true,
};
println!(
"玩家{},等级{},生命值{:.1}%",
player1.username, player1.level, player1.health
);
}
用点号(.)访问字段,简单直接。
1.2 可变结构体
如果要修改结构体的字段,整个实例必须是mut的——Rust不允许只让某些字段可变:
fn main() {
let mut player1 = Player {
username: String::from("DragonSlayer"),
level: 42,
health: 98.5,
is_online: true,
};
player1.level = 43; // 升级!
player1.health = 100.0; // 回满血
println!("恭喜升到{}级!", player1.level);
}
1.3 字段初始化简写
如果变量名和字段名相同,可以省略冒号后面的部分:
fn create_player(username: String, level: u32) -> Player {
Player {
username, // 等同于 username: username
level, // 等同于 level: level
health: 100.0,
is_online: true,
}
}
1.4 结构体更新语法
基于已有实例创建新实例,只修改部分字段:
fn main() {
let player1 = create_player(String::from("Warrior"), 10);
let player2 = Player {
username: String::from("Mage"),
..player1 // 其余字段从player1复制/移动
};
// 注意:使用 ..player1 时,Copy类型字段(health、is_online等)被复制,
// 非Copy类型字段(如String)会被移动。由于我们单独指定了username,
// player1这里没有被移动的字段,仍然可以正常使用。
// 但如果没有单独指定username,player1的username就会被移动,
// 导致player1部分失效(partial move),此时只能访问其Copy类型字段。
println!("新玩家:{}", player2.username);
}
1.5 元组结构体和单元结构体
有时候你只需要给类型一个名字,不需要给字段命名:
// 元组结构体:有名字,字段没名字
struct Color(u8, u8, u8);
struct Coordinate(f64, f64);
fn main() {
let red = Color(255, 0, 0);
let origin = Coordinate(0.0, 0.0);
println!("红色的R值:{}", red.0);
println!("原点坐标:({}, {})", origin.0, origin.1);
}
// 单元结构体:没有任何字段,常用于实现trait
struct Marker;
1.6 给结构体添加方法——impl块
Rust没有class,但struct可以通过impl块拥有方法:
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// 关联函数(类似其他语言的静态方法/构造函数)
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
// 方法(第一个参数是&self)
fn area(&self) -> f64 {
self.width * self.height
}
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
fn is_square(&self) -> bool {
(self.width - self.height).abs() < f64::EPSILON
}
// 可变方法(第一个参数是&mut self)
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
}
fn main() {
let mut rect = Rectangle::new(10.0, 5.0);
println!("面积:{}", rect.area());
println!("周长:{}", rect.perimeter());
println!("是正方形吗?{}", rect.is_square());
rect.scale(2.0);
println!("放大后面积:{}", rect.area());
}
几个关键点:
Self是当前类型的别名(这里等同于Rectangle)&self是self: &Self的语法糖——不可变借用当前实例&mut self允许修改当前实例Rectangle::new()用双冒号调用——这是关联函数(没有self参数),常用作构造函数
🤔 想一想 Rust为什么不用class而用struct + impl的方式?想想”数据”和”行为”分开定义的好处——你可以在不同的地方为同一个struct添加不同的impl块,甚至可以为别人的struct实现自己的trait(后面会讲)。
二、枚举(Enum)——“多选一”的数据类型
2.1 基础枚举
如果说结构体是”把多个数据打包在一起”,那枚举就是”在多种可能中选一个”:
enum Season {
Spring,
Summer,
Autumn,
Winter,
}
fn weather_tip(season: Season) {
match season {
Season::Spring => println!("春天来了,万物复苏"),
Season::Summer => println!("注意防晒,多喝水"),
Season::Autumn => println!("天凉了,加件外套"),
Season::Winter => println!("出门记得戴围巾"),
}
}
fn main() {
weather_tip(Season::Summer);
}
到这里还像其他语言的枚举。但Rust的枚举有一个杀手锏——
2.2 枚举变体可以携带数据
Rust的枚举每个变体可以存储不同类型、不同数量的数据。这远远超出了C/Java枚举的能力:
enum Message {
Quit, // 不携带数据
Echo(String), // 携带一个String
Move { x: i32, y: i32 }, // 携带命名字段(类似匿名struct)
Color(u8, u8, u8), // 携带三个u8
}
fn process_message(msg: Message) {
match msg {
Message::Quit => println!("收到退出指令"),
Message::Echo(text) => println!("回声:{}", text),
Message::Move { x, y } => println!("移动到({}, {})", x, y),
Message::Color(r, g, b) => println!("颜色:#{:02X}{:02X}{:02X}", r, g, b),
}
}
fn main() {
process_message(Message::Echo(String::from("你好世界")));
process_message(Message::Move { x: 100, y: 200 });
process_message(Message::Color(255, 128, 0));
process_message(Message::Quit);
}
这种设计把”类型”和”数据”合二为一,非常强大。在其他语言中实现同样的功能,你可能需要定义一个接口加上多个实现类。
2.3 最重要的两个枚举:Option和Result
Rust标准库中有两个枚举,你几乎在每个程序中都会用到:
Option:处理”有或没有”
enum Option<T> {
Some(T), // 有值
None, // 没有值
}
Rust没有null。取而代之的是Option——当一个值可能不存在时,必须用Option包装。这迫使你在使用值之前先检查它是否存在。
fn find_student(id: u32) -> Option<String> {
match id {
1 => Some(String::from("张三")),
2 => Some(String::from("李四")),
_ => None,
}
}
fn main() {
match find_student(1) {
Some(name) => println!("找到了:{}", name),
None => println!("查无此人"),
}
// 更简洁的写法
if let Some(name) = find_student(2) {
println!("学生:{}", name);
}
}
Result:处理”成功或失败”
enum Result<T, E> {
Ok(T), // 成功,携带结果值
Err(E), // 失败,携带错误信息
}
我们会在第六章详细讲解Result的用法。
2.4 给枚举添加方法
和结构体一样,枚举也可以有impl块:
#[derive(Debug)]
enum TrafficLight {
Red,
Yellow,
Green,
}
impl TrafficLight {
fn duration(&self) -> u32 {
match self {
TrafficLight::Red => 60,
TrafficLight::Yellow => 5,
TrafficLight::Green => 45,
}
}
fn next(&self) -> TrafficLight {
match self {
TrafficLight::Red => TrafficLight::Green,
TrafficLight::Green => TrafficLight::Yellow,
TrafficLight::Yellow => TrafficLight::Red,
}
}
}
fn main() {
let light = TrafficLight::Red;
println!("红灯持续{}秒", light.duration());
let next_light = light.next();
println!("下一个灯持续{}秒", next_light.duration());
}
🤔 想一想 没有null意味着什么?在Java中,NullPointerException是最常见的运行时错误之一。Rust用Option
替代null后,为什么就不会出现空指针异常了?
三、模式匹配——Rust的”超级分拣机”
3.1 match的穷尽性检查
match的一个硬性要求:必须覆盖所有可能的情况。编译器会帮你检查是否遗漏:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
// 如果这里少了Quarter,编译器会报错!
Coin::Quarter => 25,
}
}
这就像海关检查行李——每一种物品都必须有明确的处理规则,不允许”我不知道怎么处理”的情况出现。
3.2 通配符与默认分支
当你只关心其中几种情况时,用_捕获剩余的所有情况:
fn describe_number(n: i32) -> &'static str {
match n {
0 => "零",
1 => "壹",
2..=9 => "个位数",
10..=99 => "两位数",
_ => "很大的数",
}
}
3.3 解构与绑定
模式匹配可以”拆开”复合数据,提取内部的值:
struct Point {
x: f64,
y: f64,
}
fn classify_point(p: &Point) {
// 注意:Rust 不允许在 match 模式中使用浮点数字面量(因为浮点数只实现了 PartialEq 而非 Eq,
// match 模式要求精确相等),需要用 match guard
match p {
Point { x, y } if *x == 0.0 && *y == 0.0 => println!("原点"),
Point { x, y } if *y == 0.0 => println!("在X轴上,x={}", x),
Point { x, y } if *x == 0.0 => println!("在Y轴上,y={}", y),
Point { x, y } => println!("普通点({}, {})", x, y),
}
}
3.4 if let——当你只关心一种情况
如果你只想处理一种匹配,不关心其他情况,if let比match更简洁:
fn main() {
let favorite_color: Option<&str> = Some("蓝色");
// 用match(略显冗余)
match favorite_color {
Some(color) => println!("你最喜欢的颜色是{}", color),
None => (),
}
// 用if let(更简洁)
if let Some(color) = favorite_color {
println!("你最喜欢的颜色是{}", color);
}
}
3.5 while let——循环版的if let
fn main() {
let mut stack = vec![1, 2, 3, 4, 5];
while let Some(top) = stack.pop() {
println!("弹出:{}", top);
}
println!("栈已清空");
}
3.6 复杂模式匹配示例
enum Command {
Attack { target: String, power: u32 },
Defend,
Heal(u32),
Flee,
}
fn execute(cmd: Command) {
match cmd {
Command::Attack { target, power } if power > 50 => {
println!("对{}发动强力攻击!伤害:{}", target, power);
}
Command::Attack { target, power } => {
println!("对{}发动普通攻击,伤害:{}", target, power);
}
Command::Defend => println!("举盾防御!"),
Command::Heal(amount) if amount > 30 => {
println!("强力治疗!恢复{}点生命", amount);
}
Command::Heal(amount) => {
println!("轻微治疗,恢复{}点生命", amount);
}
Command::Flee => println!("三十六计走为上计!"),
}
}
fn main() {
execute(Command::Attack {
target: String::from("恶龙"),
power: 80,
});
execute(Command::Heal(15));
execute(Command::Flee);
}
注意if power > 50这样的匹配守卫(match guard)——它在模式匹配的基础上增加了额外的条件判断,让分支逻辑更加精细。
⚠️ 常见误区
- 忘了match必须穷尽所有可能——如果你的枚举有5个变体,match必须处理全部5个(或者用
_兜底)。这不是麻烦,是保护——当你给枚举新增一个变体时,编译器会告诉你所有需要更新的地方。- 过度使用clone来回避所有权问题——在struct更新语法中,如果被”借用”的字段是String等Move类型,原结构体会部分失效。新手常用clone回避这个问题,但更好的做法是仔细设计数据的所有权归属。
- 把Option当成null来用——不要到处unwrap()!
unwrap()在值为None时会panic。正确的做法是用match、if let或者?运算符来安全地处理Option。- 混淆方法和关联函数——有
&self参数的是方法(用.调用),没有的是关联函数(用::调用)。Rectangle::new()是关联函数,rect.area()是方法。
📝 掌握度自测
- 结构体:定义一个
Book结构体(title、author、pages、price字段),为它实现一个describe()方法,输出书的信息。 - 枚举:定义一个
Shape枚举,包含Circle(f64)(半径)、Rectangle(f64, f64)(宽高)、Triangle(f64, f64, f64)(三边长),为它实现一个area()方法。 - 模式匹配:解释为什么下面的代码无法编译,并修复它:
enum Direction { North, South, East, West } fn go(d: Direction) { match d { Direction::North => println!("向北"), Direction::South => println!("向南"), } } - Option:写一个函数
safe_divide(a: f64, b: f64) -> Option<f64>,当除数为0时返回None,否则返回Some(结果)。 - 综合:用枚举+模式匹配实现一个简易计算器:定义
Operation枚举(Add、Sub、Mul、Div),写一个calculate(op: Operation, a: f64, b: f64) -> Option<f64>函数。
💡 自我评估
- 答对5题:结构体和枚举已经得心应手,你对Rust的类型系统有了扎实的理解。
- 答对3-4题:核心概念已掌握,模式匹配的高级用法可能还需要多练习。
- 答对0-2题:建议把每个代码示例都在编辑器中运行一遍,尤其关注枚举携带数据和match的穷尽性检查。
购买课程解锁全部内容
内存安全 + 零成本抽象:Rust 系统编程实战
¥29.90