第四章:结构体、枚举与模式匹配——数据的艺术
本章导读:程序的本质是处理数据。如何组织数据,决定了程序的清晰程度和可维护性。Rust 提供了强大的工具来建模数据:结构体让你组合相关数据,枚举让你表达互斥的可能性,模式匹配让你优雅地处理各种情况。本章将带你深入这些核心概念,展示 Rust 如何用类型系统帮助你写出更健壮的代码。
🏗️ 4.1 结构体:数据的容器
结构体(Struct)是一种自定义数据类型,让你可以将多个相关的值组合成一个有意义的整体。
📦 4.1.1 定义与实例化
最基本的结构体定义:
struct User {
username: String,
email: String,
active: bool,
sign_in_count: u64,
}
创建结构体实例:
let user1 = User {
email: String::from("user@example.com"),
username: String::from("张三"),
active: true,
sign_in_count: 1,
};
访问字段使用点号:
println!("用户邮箱: {}", user1.email);
命名约定:结构体名使用
PascalCase(如User、HttpRequest),字段和方法名使用snakecase</code>(如 <code>signincount</code>、<code>sendrequest)。这是 Rust 社区的一致约定。
🔄 4.1.2 结构体更新语法
当你需要基于一个现有实例创建新实例,只修改部分字段时,可以使用 .. 语法:
let user2 = User {
email: String::from("another@example.com"),
..user1 // 其余字段来自 user1
};
注意 .. 语法会移动结构体中的值。由于 username 是 String 类型,user1.username 被移动到 user2,之后不能使用 user1.username。但 active 和 signincount 是 Copy 类型,可以继续使用。
⚡ 4.1.3 元组结构体
当你想给元组一个名字,但不需要命名字段时:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
即使两个字段类型完全相同,Color 和 Point 也是不同的类型,不能混用。这可以防止将颜色当作坐标使用之类的逻辑错误。
🎭 4.1.4 单元结构体
没有字段的结构体:
struct AlwaysEqual;
这种结构体在需要实现 trait 但不需要存储数据时很有用。我们会在后续章节看到实际应用。
🎬 4.2 方法定义
方法是依附于结构体(或枚举、trait 对象)的函数,它的第一个参数总是 self,代表调用该方法的结构体实例。
📍 4.2.1 定义方法
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// 关联函数(没有 self)
fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
// 方法(有 self)
fn area(&self) -> u32 {
self.width * self.height
}
// 方法(可变借用 self)
fn scale(&mut self, factor: u32) {
self.width *= factor;
self.height *= factor;
}
// 方法(获取 self 所有权)
fn destroy(self) {
println!("销毁 {}x{} 的矩形", self.width, self.height);
}
}
fn main() {
let mut rect = Rectangle::new(10, 20); // 调用关联函数
println!("面积: {}", rect.area()); // 调用方法
rect.scale(2);
println!("缩放后: {:?}", rect);
rect.destroy();
// rect.area(); // 错误!rect 已被移动
}
🏠 4.2.2 impl 块
所有与类型相关的方法和关联函数都放在 impl 块中。一个类型可以有多个 impl 块——这在写大型类型或使用宏生成代码时很有用。
🎯 4.2.3 关联函数 vs 方法
| 关联函数 | 方法 |
|---|---|
没有 self 参数 | 第一个参数是 self |
像 String::from() 这样调用 | 像 s.len() 这样调用 |
| 常用作构造函数 | 操作实例数据 |
使用 Type::function() 语法 | 使用 instance.method() 语法 |
⬅️ 4.2.4 self 的三种形式
| 形式 | 含义 | 使用场景 |
|---|---|---|
&self | 不可变借用 | 只读操作 |
&mut self | 可变借用 | 修改实例 |
self | 获取所有权 | 转换或销毁实例 |
第一性原理:为什么方法默认借用而不是移动?因为绝大多数情况下,调用方法后你还会继续使用对象。如果方法默认获取所有权,一个简单的
rect.area()就会消耗rect,这显然不合理。
🎨 4.3 枚举:可能性的表达
枚举(Enum)允许你定义一个类型,它可以是几种不同变体之一。与结构体组合数据不同,枚举表达的是"非此即彼"的关系。
🔢 4.3.1 基本枚举
enum Direction {
Up,
Down,
Left,
Right,
}
fn move_player(dir: Direction) {
match dir {
Direction::Up => println!("向上移动"),
Direction::Down => println!("向下移动"),
Direction::Left => println!("向左移动"),
Direction::Right => println!("向右移动"),
}
}
📦 4.3.2 带数据的枚举
枚举的真正威力在于每个变体可以携带不同类型的数据:
enum Message {
Quit, // 没有数据
Move { x: i32, y: i32 }, // 匿名结构体
Write(String), // String
ChangeColor(i32, i32, i32), // 三个 i32
}
fn process_message(msg: Message) {
match msg {
Message::Quit => {
println!("退出游戏");
}
Message::Move { x, y } => {
println!("移动到 ({}, {})", x, y);
}
Message::Write(text) => {
println!("发送消息: {}", text);
}
Message::ChangeColor(r, g, b) => {
println!("颜色变为 RGB({}, {}, {})", r, g, b);
}
}
}
费曼技巧提问:如何向非程序员解释枚举?想象一个问卷调查问题:"您的婚姻状况是?"答案只能是"单身"、"已婚"、"离异"、"丧偶"之一——这就是枚举,每个答案都是枚举的一个变体。而"已婚"可能需要附加"配偶姓名"这样的额外信息——这就是带数据的枚举。
🌟 4.3.3 Option:空值的正确处理
Rust 没有空值(null),而是使用 Option 枚举来表示可能缺失的值:
enum Option<T> {
Some(T),
None,
}
这意味着你不能意外地访问空值——编译器强制你处理"没有值"的情况:
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("张三"))
} else {
None
}
}
fn main() {
let user = find_user(1);
// 必须处理 None 的情况
match user {
Some(name) => println!("找到用户: {}", name),
None => println!("用户不存在"),
}
}
为什么没有 null? Tony Hoare(快速排序的发明者)称 null 是他的"十亿美元错误"。null 的问题在于它可以在任何地方出现,但程序员常常忘记检查它。Rust 的
Option让"可能为空"成为类型系统的一部分,编译器会确保你处理了所有情况。
⚡ 4.3.4 Result:错误的优雅处理
与 Option 类似,Result 用于表示可能失败的操作:
enum Result<T, E> {
Ok(T),
Err(E),
}
Ok 包含成功的值,Err 包含错误信息:
use std::fs::File;
use std::io::Read;
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?; // ? 操作符自动传播错误
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() {
match read_file("data.txt") {
Ok(content) => println!("文件内容:\n{}", content),
Err(e) => println!("读取失败: {}", e),
}
}
🎯 4.4 模式匹配:解构的力量
模式匹配是 Rust 最强大的特性之一,它让你可以根据数据的形状来分支代码。
🎲 4.4.1 match 表达式
match 是穷尽的——必须处理所有可能的情况:
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState), // 25美分带州信息
}
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// ...
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("来自 {:?} 的 25 美分!", state);
25
}
}
}
match 是表达式,可以返回值:
let value = match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(_) => 25, // _ 忽略州信息
};
🎭 4.4.2 if let:简化匹配
当你只关心一种情况,不想写完整的 match 时:
let some_value = Some(3);
// 使用 match
match some_value {
Some(3) => println!("三!"),
_ => (), // 忽略其他情况
}
// 使用 if let(更简洁)
if let Some(3) = some_value {
println!("三!");
}
if let 可以配合 else:
if let Some(3) = some_value {
println!("三!");
} else {
println!("不是三");
}
🔗 4.4.3 while let:循环匹配
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top);
}
// 输出: 3, 2, 1
📦 4.4.4 解构结构体和元组
struct Point {
x: i32,
y: i32,
}
let point = Point { x: 10, y: 20 };
// 解构结构体
let Point { x, y } = point;
println!("({}, {})", x, y);
// 带条件的解构
match point {
Point { x: 0, y } => println!("在 Y 轴上,y = {}", y),
Point { x, y: 0 } => println!("在 X 轴上,x = {}", x),
Point { x, y } => println!("在其他位置 ({}, {})", x, y),
}
// 解构元组
let tuple = (1, "hello", 3.14);
let (a, b, c) = tuple;
println!("{} {} {}", a, b, c);
📋 4.4.5 let 模式
let 语句本身也是一种模式匹配:
let (x, y, z) = (1, 2, 3); // 解构元组
let [a, b, c] = [1, 2, 3]; // 解构数组
let (p, q) = (1,); // 错误!类型不匹配
🚫 4.4.6 忽略值:_ 和 ..
_ 匹配任意值但不绑定:
fn foo(_: i32, y: i32) {
println!("只使用 y: {}", y);
}
match some_value {
First => println!("第一个"),
_ => println!("其他"), // 忽略具体值
}
.. 忽略剩余部分:
struct Point3D {
x: i32,
y: i32,
z: i32,
}
let point = Point3D { x: 1, y: 2, z: 3 };
match point {
Point3D { x, .. } => println!("x 坐标是 {}", x),
}
let numbers = (1, 2, 3, 4, 5);
match numbers {
(first, .., last) => {
println!("第一个: {}, 最后一个: {}", first, last);
}
}
🏆 4.5 实战:JSON解析器的数据模型
让我们用一个实际例子来综合运用本章的知识。假设我们要为一个简化的 JSON 格式建立数据模型:
/// JSON 值的类型
#[derive(Debug, Clone)]
enum JsonValue {
Null,
Bool(bool),
Number(f64),
String(String),
Array(Vec<JsonValue>),
Object(Vec<(String, JsonValue)>), // 键值对列表
}
impl JsonValue {
/// 从布尔值创建
fn from_bool(b: bool) -> Self {
JsonValue::Bool(b)
}
/// 从数字创建
fn from_number(n: f64) -> Self {
JsonValue::Number(n)
}
/// 从字符串创建
fn from_string(s: String) -> Self {
JsonValue::String(s)
}
/// 获取字符串值(如果存在)
fn as_str(&self) -> Option<&str> {
match self {
JsonValue::String(s) => Some(s),
_ => None,
}
}
/// 获取数字值(如果存在)
fn as_number(&self) -> Option<f64> {
match self {
JsonValue::Number(n) => Some(*n),
_ => None,
}
}
/// 获取数组长度(如果是数组)
fn array_len(&self) -> Option<usize> {
match self {
JsonValue::Array(arr) => Some(arr.len()),
_ => None,
}
}
/// 获取对象中指定键的值
fn get(&self, key: &str) -> Option<&JsonValue> {
match self {
JsonValue::Object pairs => {
for (k, v) in pairs {
if k == key {
return Some(v);
}
}
None
}
_ => None,
}
}
}
fn main() {
let person = JsonValue::Object(vec![
("name".to_string(), JsonValue::String("张三".to_string())),
("age".to_string(), JsonValue::Number(30.0)),
("married".to_string(), JsonValue::Bool(false)),
]);
if let JsonValue::Object(pairs) = &person {
for (key, value) in pairs {
println!("{}: {:?}", key, value);
}
}
if let Some(name) = person.get("name").and_then(|v| v.as_str()) {
println!("姓名: {}", name);
}
}
这个例子展示了:
- 枚举表达不同类型的 JSON 值
- 方法提供方便的访问和转换
- 模式匹配提取嵌套数据
- <code>Option</code> 处理可能不存在的值
📝 本章小结
本章我们学习了 Rust 组织数据的三大工具:
- 结构体组合相关数据,可以命名每个字段
- 枚举表达互斥的可能性,每个变体可以携带不同数据
- 模式匹配优雅地处理各种情况,编译器确保穷尽性
关键要点:
Option<T>替代空值,强制处理"没有值"的情况Result<T, E>表示可能失败的操作match必须处理所有情况,if let简化单分支匹配- 模式可以解构结构体、元组、数组等复杂类型
在下一章,我们将学习泛型和 Trait——这是 Rust 实现代码复用和多态的核心机制。
动手实验:
- 定义一个
Vehicle枚举,包含Car(带座位数)、Motorcycle(带排量)和Bicycle(无数据)三种变体。实现一个方法返回每种交通工具的描述。- 使用
if let重写以下代码,使其更简洁:``
rust match maybenumber { Some(n) if n > 0 => println!("正数: {}", n), => {} }``
- 实现一个简单的表达式求值器:定义
Expr枚举表示数字、加法和乘法,实现eval方法计算表达式的值。