第四章:结构体、枚举与模式匹配——数据的艺术

第四章:结构体、枚举与模式匹配——数据的艺术

本章导读:程序的本质是处理数据。如何组织数据,决定了程序的清晰程度和可维护性。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(如 UserHttpRequest),字段和方法名使用 snakecase</code>(如 <code>signincount</code>、<code>sendrequest)。这是 Rust 社区的一致约定。

🔄 4.1.2 结构体更新语法

当你需要基于一个现有实例创建新实例,只修改部分字段时,可以使用 .. 语法:

let user2 = User {
    email: String::from("another@example.com"),
    ..user1  // 其余字段来自 user1
};

注意 .. 语法会移动结构体中的值。由于 usernameString 类型,user1.username 被移动到 user2,之后不能使用 user1.username。但 activesignincountCopy 类型,可以继续使用。

⚡ 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);

即使两个字段类型完全相同,ColorPoint 也是不同的类型,不能混用。这可以防止将颜色当作坐标使用之类的逻辑错误。

🎭 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 实现代码复用和多态的核心机制。


动手实验

  1. 定义一个 Vehicle 枚举,包含 Car(带座位数)、Motorcycle(带排量)和 Bicycle(无数据)三种变体。实现一个方法返回每种交通工具的描述。
  2. 使用 if let 重写以下代码,使其更简洁:

``rust match maybenumber { Some(n) if n &gt; 0 =&gt; println!(&quot;正数: {}&quot;, n), => {} } ``

  1. 实现一个简单的表达式求值器:定义 Expr 枚举表示数字、加法和乘法,实现 eval 方法计算表达式的值。
← 返回目录