3Rust程序设计语言

结构体与枚举

自定义类型

结构体枚举模式匹配

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第三章 结构体与枚举 (Structs and Enums)

一、导读

1.1 本章学习目标

掌握 Rust 结构体(struct)的三种形式:命名字段结构体、元组结构体和单元结构体

理解结构体方法的定义方式,区分关联函数(associated function)与实例方法(method)

掌握枚举(enum)的定义与模式匹配(pattern matching),理解 match 表达式的穷举性

理解 Option<T> 和 Result<T, E> 枚举的设计哲学及其在错误处理中的核心地位

掌握 impl 块的使用方式,学会为自定义类型添加行为

理解枚举的"代数数据类型"(Algebraic Data Type)本质,掌握和类型与积类型的概念

掌握 if let 和 while let 语法的使用场景

能够使用结构体和枚举设计符合 Rust 惯用法的数据模型

1.2 为什么本章如此重要

结构体和枚举是 Rust 中定义自定义数据类型的两大基石。如果说所有权系统是 Rust 的灵魂,那么结构体和枚举就是 Rust 的骨肉——它们决定了你如何组织和表达程序中的数据。几乎所有有意义的 Rust 程序都需要定义自己的结构体和枚举来建模业务领域。

与面向对象语言中的"类"不同,Rust 的结构体只包含数据,不包含行为。行为通过 impl 块与结构体关联,这种数据与行为的分离设计使得代码组织更加灵活。一个结构体可以有多个 impl 块,不同的 impl 块可以定义在不同的文件中,甚至可以由不同的库为同一类型添加方法(通过 trait)。

枚举是 Rust 类型系统中最强大的特性之一。与 C 语言的枚举(仅仅是整数的别名)完全不同,Rust 的枚举是"代数数据类型"(ADT),每个变体(variant)可以携带不同类型和数量的数据。结合模式匹配,枚举提供了一种优雅且安全的方式来处理多种可能的情况。Rust 标准库中的 Option<T> 和 Result<T, E> 两个枚举彻底改变了错误处理和空值处理的方式,消除了空指针异常和未检查错误这两类最常见的运行时错误。

1.3 核心问题引导

Rust 的结构体与 Java/C++ 的类有什么本质区别?为什么 Rust 选择将数据和方法分离?

枚举的每个变体为什么可以携带不同类型的数据?这与"标签联合"(tagged union)有什么关系?

match 表达式为什么要求穷举所有可能的模式?这种设计如何防止遗漏边界情况?

Option<T> 如何替代 null 引用?它比 null 更安全的原因是什么?

什么是"代数数据类型"?和类型(sum type)与积类型(product type)分别对应什么?

if let 相比 match 在什么场景下更合适?它们各自的优势是什么?


二、核心概念详解

3.1 结构体的定义与使用

Rust 提供三种结构体形式,适用于不同的场景。

命名字段结构体是最常用的形式,每个字段都有名称和类型:

rust
struct User {
    name: String,
    email: String,
    age: u32,
    active: bool,
}

创建实例时使用字段初始化语法,Rust 还提供了字段初始化简写(当变量名与字段名相同时):

rust
let user = User {
    name: String::from("Alice"),
    email: String::from("alice@example.com"),
    age: 30,
    active: true,
};

更新语法(struct update syntax)允许基于已有实例创建新实例:

rust
let user2 = User {
    name: String::from("Bob"),
    ..user // 其余字段从 user 复制
};

元组结构体没有字段名,只有类型,适用于需要给元组一个名字的场景:

rust
struct Point(f64, f64);
struct Color(u8, u8, u8);

let p = Point(3.0, 4.0);
let red = Color(255, 0, 0);

单元结构体没有任何字段,适用于需要为某个类型实现 trait 但不需要存储数据的场景:

rust
struct Marker;

3.2 方法定义与 impl 块

通过 impl 块为结构体定义方法。方法的第一个参数是实例本身,有三种形式:&self(不可变借用)、&mut self(可变借用)和 self(获取所有权)。

rust
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // 关联函数(构造函数)
    fn new(width: f64, height: f64) -> Self {
        Rectangle { width, height }
    }
    
    // 实例方法(不可变借用)
    fn area(&self) -> f64 {
        self.width * self.height
    }
    
    // 实例方法(可变借用)
    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }
    
    // 判断方法
    fn is_square(&self) -> bool {
        (self.width - self.height).abs() < f64::EPSILON
    }
}

关联函数(如 new)没有 self 参数,通过 :: 语法调用:Rectangle::new(10.0, 5.0)。它们通常用于创建实例的工厂函数。

多个 impl 块:Rust 允许为同一类型定义多个 impl 块,这在组织代码和条件编译时非常有用:

rust
impl Rectangle {
    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }
}

3.3 枚举与变体

Rust 的枚举是真正的代数数据类型,每个变体可以携带不同的数据:

rust
enum Shape {
    Circle(f64),                    // 元组变体
    Rectangle { width: f64, height: f64 }, // 结构体变体
    Point,                          // 单元变体
}

枚举变体在命名空间中通过 :: 语法访问:

rust
let circle = Shape::Circle(5.0);
let rect = Shape::Rectangle { width: 10.0, height: 5.0 };

枚举可以为所有变体定义公共方法:

rust
impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => std::f64::consts::PI * r * r,
            Shape::Rectangle { width, height } => width * height,
            Shape::Point => 0.0,
        }
    }
}

3.4 模式匹配与 match

match 是 Rust 中最强大的控制流结构,它要求穷举所有可能的模式。编译器会在编译期检查是否所有情况都被处理,遗漏任何情况都会导致编译错误。

rust
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

匹配绑定:match 可以在匹配的同时绑定变量:

rust
fn describe_number(x: Option<i32>) -> String {
    match x {
        Some(n) if n < 0 => format!("{} 是负数", n),
        Some(0) => String::from("零"),
        Some(n) => format!("{} 是正数", n),
        None => String::from("没有值"),
    }
}

通配符 `_`:当不需要穷举所有情况时,使用 _ 匹配剩余情况:

rust
match value {
    1 => println!("one"),
    2 => println!("two"),
    _ => (), // 忽略其他情况
}

3.5 Option 与 Result 枚举

Option<T> 和 Result<T, E> 是 Rust 标准库中最重要的两个枚举,它们替代了 null 和异常。

rust
enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Option<T> 表示"可能有值也可能没有",替代了其他语言中的 null。由于 Rust 强制要求处理 None 的情况,空引用错误在编译期就被捕获。

rust
fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("Alice"))
    } else {
        None
    }
}

// 使用 match 处理
match find_user(1) {
    Some(name) => println!("找到用户:{}", name),
    None => println!("用户不存在"),
}

Result<T, E> 表示操作可能成功或失败,是 Rust 错误处理的基础。


三、深入分析与讨论

3.1 代数数据类型:和类型与积类型

从类型理论的角度看,结构体是"积类型"(Product Type),枚举是"和类型"(Sum Type)。积类型的值数量是各字段值数量的乘积——一个有 3 个 bool 字段的结构体有 2×2×2=8 种可能的值。和类型的值数量是各变体值数量的总和——一个有 3 个无数据变体的枚举恰好有 3 种可能的值。

这种数学基础使得 Rust 的类型系统能够精确地建模业务领域的状态空间。通过精心设计结构体和枚举的组合,可以让"不可能的状态不可表示"(make impossible states unrepresentable)。例如,一个"用户注册状态"不应该用两个独立的布尔值表示(因为会产生 4 种状态,其中一些是无意义的),而应该用枚举表示(只有 3 种有效状态:未注册、已注册未验证、已注册已验证)。

3.2 模式匹配的穷举性与安全性

match 的穷举性检查是 Rust "如果它编译通过,它就是正确的"理念的重要体现。当你在枚举中添加新变体时,编译器会指出所有需要更新的 match 表达式,确保没有遗漏。这种"编译期穷举检查"在大型项目中尤其有价值——它使得代码重构更加安全。

与 switch 语句不同,match 是表达式(有返回值),并且支持"守卫"(guard)条件、嵌套解构和绑定。这些特性使得 match 不仅能替代 switch,还能表达更复杂的条件逻辑。

3.3 结构体与枚举的组合设计

在实际项目中,结构体和枚举经常组合使用。枚举的变体可以包含结构体,结构体的字段可以是枚举。这种组合使得数据建模既灵活又安全。

rust
struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

enum IpAddrKind {
    V4,
    V6,
}

// 更简洁的写法:将数据直接放入枚举变体
enum IpAddr2 {
    V4(String),
    V6(String),
}

四、实践案例

4.1 实现一个简单的图形计算器

使用枚举和结构体建模不同图形,计算面积和周长。

rust
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { a: f64, b: f64, c: f64 },
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => std::f64::consts::PI * radius.powi(2),
            Shape::Rectangle { width, height } => width * height,
            Shape::Triangle { a, b, c } => {
                let s = (a + b + c) / 2.0;
                (s * (s - a) * (s - b) * (s - c)).sqrt()
            }
        }
    }

    fn perimeter(&self) -> f64 {
        match self {
            Shape::Circle { radius } => 2.0 * std::f64::consts::PI * radius,
            Shape::Rectangle { width, height } => 2.0 * (width + height),
            Shape::Triangle { a, b, c } => a + b + c,
        }
    }

    fn is_valid(&self) -> bool {
        match self {
            Shape::Circle { radius } => *radius > 0.0,
            Shape::Rectangle { width, height } => *width > 0.0 && *height > 0.0,
            Shape::Triangle { a, b, c } => a + b > c && a + c > b && b + c > a,
        }
    }
}

fn main() {
    let shapes = vec![
        Shape::Circle { radius: 5.0 },
        Shape::Rectangle { width: 10.0, height: 5.0 },
        Shape::Triangle { a: 3.0, b: 4.0, c: 5.0 },
    ];

    for shape in &shapes {
        if shape.is_valid() {
            println!("面积: {:.2}, 周长: {:.2}", shape.area(), shape.perimeter());
        }
    }
}

4.2 命令行任务管理器

使用结构体建模任务,使用枚举管理任务状态。

rust
#[derive(Debug)]
enum Status {
    Todo,
    InProgress,
    Done,
}

#[derive(Debug)]
struct Task {
    title: String,
    status: Status,
    priority: u8,
}

impl Task {
    fn new(title: &str, priority: u8) -> Self {
        Task {
            title: String::from(title),
            status: Status::Todo,
            priority,
        }
    }

    fn advance(&mut self) {
        self.status = match self.status {
            Status::Todo => Status::InProgress,
            Status::InProgress => Status::Done,
            Status::Done => Status::Done,
        };
    }

    fn status_text(&self) -> &str {
        match self.status {
            Status::Todo => "待办",
            Status::InProgress => "进行中",
            Status::Done => "已完成",
        }
    }
}

fn main() {
    let mut tasks = vec![
        Task::new("学习 Rust", 1),
        Task::new("写项目", 2),
        Task::new("复习", 3),
    ];

    tasks[0].advance();
    
    for task in &tasks {
        println!("[{}] {} (优先级: {})", task.status_text(), task.title, task.priority);
    }
}

五、常见误区与注意事项

忘记为枚举变体实现所有分支:match 要求穷举所有情况。添加新变体后,必须更新所有相关的 match 表达式。可以使用 _ 通配符避免编译错误,但这样做会失去穷举性检查的保护。

混淆关联函数和实例方法:关联函数没有 self 参数,通过 Type::function() 调用;实例方法有 self 参数,通过 instance.method() 调用。构造函数通常是关联函数。

过度使用 `unwrap()`:对 Option 和 Result 使用 unwrap() 在遇到 None 或 Err 时会 panic。生产代码中应使用 match、if let、? 运算符或 unwrap_or() 等方法安全地处理。

结构体字段使用 `String` 而非 `&str`:当结构体拥有数据时使用 String,当结构体只是借用数据时使用 &str。选择取决于结构体是否需要数据的所有权。

枚举变体数据过大导致内存浪费:枚举的大小由其最大变体决定。如果某个变体包含大量数据,考虑使用 Box<T> 将其放到堆上,以减小枚举的整体大小。


六、本章小结

本章深入讲解了 Rust 的结构体和枚举两大自定义数据类型。结构体通过命名字段、元组和单元三种形式,提供了灵活的数据组织方式。impl 块将行为与数据关联,关联函数和实例方法的区分使得代码意图更加清晰。枚举作为代数数据类型,每个变体可以携带不同类型的数据,配合 match 表达式的穷举性检查,提供了安全且表达力强大的模式匹配能力。Option<T> 和 Result<T, E> 两个标准库枚举从根本上改变了空值处理和错误处理的方式,将运行时错误前移到编译期。结构体与枚举的组合使用是 Rust 数据建模的核心技能。


七、思考题与练习

设计一个 Book 结构体,包含书名、作者、ISBN 和价格字段。为它实现 display_info 方法和 apply_discount 方法。

使用枚举建模一个"支付方式"类型,支持信用卡(含卡号和有效期)、借记卡(含卡号)、电子钱包(含用户名和余额)和现金(无数据)。为每种方式实现 transaction_fee 方法。

解释为什么 match 必须穷举所有模式。如果允许不穷举,可能导致什么问题?给出一个具体的例子。

使用 Option<i32> 实现一个 divide 函数,当除数为零时返回 None。分别使用 match 和 if let 处理返回值。

设计一个状态机,使用枚举表示状态,结构体存储上下文数据。实现一个"交通信号灯"的状态转换逻辑。

解释"让不可能的状态不可表示"的含义。设计一个用户注册流程的数据模型,使得"已验证但未注册"这样的非法状态无法表示。

研究 #[derive(Debug)]、#[derive(Clone)] 等派生宏的作用。为自定义结构体添加适当的 derive 属性。

实现一个简易的表达式求值器,使用枚举表示不同的表达式类型(数字、加法、乘法等),使用递归方式计算结果。

3/10结构体与
0%