Agent X-Ray
RuntimeNotesAbout
Notes/代码工程/Rust 深度教材/第4章

第4章:数据建模 —— 结构体、枚举、模式匹配与方法

7 分钟 · 更新于 2026-09-01

第4章:数据建模 —— 结构体、枚举、模式匹配与方法

本章围绕“交付工作项”建立领域模型。我们不从结构体语法开始,而是先列出必须成立的规则,再选择元组、结构体、枚举、模式匹配和方法,把规则逐步压进类型系统。


一、从不变量倒推类型

假设一个交付工作项有编号、标题、优先级和状态。状态只能在“待安排、执行中、已完成”三者中选择;执行中必须记录负责人,完成后必须记录复核结论。

如果把这些信息拆成若干布尔值和可空字段,调用者很容易组合出矛盾状态。更稳妥的做法是先写不变量:

  1. 一个时刻只能处于一种状态;
  2. 只有执行中状态拥有负责人;
  3. 只有完成状态拥有复核结果;
  4. 状态转换由类型的方法统一执行。

然后再为每类关系选择工具:

关系建模工具
多个字段同时构成一个对象结构体
多种状态只能选择一种枚举
从数据形状中取出字段模式匹配
合法的状态转换方法
短期返回多个值元组
数量固定的同类元素数组

建模顺序 先写“永远必须成立什么”,再写类型;不要先创建一堆字段,最后才用注释解释哪些组合不合法。

二、元组:轻量组合,但不要承载过多语义

函数需要同时返回最小和最大工时,可以先用元组:

rust
fn estimate_range(hours: u8) -> (u8, u8) {
    (hours.saturating_sub(1), hours.saturating_add(2))
}

fn main() {
    let range = estimate_range(5);
    println!("{}..={}", range.0, range.1);

    let (min, max) = range;
    println!("最少 {min} 小时,最多 {max} 小时");
}

元组长度固定,各位置可以是不同类型;既能用 .0 访问,也能用模式解构。它适合短生命周期的临时组合,但 (u8, u8) 没有说明两个位置的业务含义。语义稳定后,应改成命名结构体:

rust
struct EstimateRange {
    min: u8,
    max: u8,
}

元组结构体则适合给基础值增加类型身份:

rust
struct WorkItemId(u64);

let id = WorkItemId(42);
println!("工作项编号:{}", id.0);

WorkItemId 与普通 u64 不再是同一类型,可以减少把用户编号、订单编号和工作项编号混用的机会。


三、结构体与数组:描述一个完整工作项

先组合「同时存在」的字段:

rust
#[derive(Debug)]
struct WorkItem {
    id: u64,
    title: String,
    estimate_hours: u8,
    tags: [String; 2],
}

let item = WorkItem {
    id: 1,
    title: String::from("整理 Rust 学习笔记"),
    estimate_hours: 6,
    tags: [String::from("学习"), String::from("Rust")],
};

println!("{}: {}", item.id, item.title);
println!("{item:#?}");

#[derive(Debug)] 让编译器生成调试输出能力。它适合开发期观察数据,不等于面向用户的正式展示格式。

当变量名与字段名相同,可以使用字段初始化简写:

rust
fn build_item(id: u64, title: String) -> WorkItem {
    WorkItem {
        id,
        title,
        estimate_hours: 1,
        tags: [String::from("待分类"), String::from("默认")],
    }
}

也可以基于旧值构造新值:

rust
let copied = WorkItem {
    id: 2,
    title: String::from("复核学习笔记"),
    ..item
};

..item 不是继承,也不保证完整复制。String 等非 Copy 字段可能被移动,旧值之后不能再作为完整值使用。

数组 [T; N] 把元素类型与长度同时写进类型。[String; 2][String; 3] 是不同类型,适合表达「恰好两个标签」。安全访问优先考虑 .get()

rust
match copied.tags.get(2) {
    Some(tag) => println!("第三个标签:{tag}"),
    None => println!("这个工作项只有两个标签"),
}

直接使用 tags[2] 越界会在运行时 panic.get(2) 返回 Option<&String>,调用者必须处理没有值的情况。若标签数量需要动态增减,应改用 Vec<T>


四、枚举:让无效状态不可表示

工作项状态不应只是三个字符串,因为不同状态携带的数据不同:

rust
#[derive(Debug, Clone, Copy)]
enum Priority {
    Low,
    Medium,
    High,
}

#[derive(Debug)]
enum WorkItemStatus {
    Todo,
    Doing { assignee: String },
    Done { needs_review: bool },
}

三个变体属于同一个 WorkItemStatus 类型,却分别携带不同数据:

rust
let todo = WorkItemStatus::Todo;
let doing = WorkItemStatus::Doing {
    assignee: String::from("Lin"),
};
let done = WorkItemStatus::Done {
    needs_review: true,
};

这样建模后:

  • Todo 根本没有负责人字段;
  • Doing 一旦存在,就一定有负责人;
  • Done 一旦存在,就一定有复盘标记;
  • 无法构造「Doing 但负责人为空」这种结构性非法状态。

把状态加入工作项:

rust
#[derive(Debug)]
struct WorkItem {
    id: u64,
    title: String,
    priority: Priority,
    status: WorkItemStatus,
    estimate_hours: u8,
    tags: [String; 2],
}

标准库的 Option<T> 也是枚举:Some(T) 表示有值,None 表示没有值。Rust 不提供可随处出现的 null,而是让「缺失」成为必须处理的类型分支。


五、match:判断、解构与返回值一次完成

给状态生成展示文本:

rust
fn status_text(status: &WorkItemStatus) -> String {
    match status {
        WorkItemStatus::Todo => String::from("待处理"),
        WorkItemStatus::Doing { assignee } => format!("进行中:{assignee}"),
        WorkItemStatus::Done { needs_review: true } => String::from("已完成,待复盘"),
        WorkItemStatus::Done { needs_review: false } => String::from("已完成"),
    }
}

match 是表达式,每个分支产生一个值。它还要求穷尽:若新增 Cancelled,编译器会指出遗漏的匹配位置。这是枚举状态比字符串状态可靠的关键原因。

只读取携带 String 的枚举时,优先匹配引用,避免移动内部数据:

rust
fn print_status(item: &WorkItem) {
    match &item.status {
        WorkItemStatus::Todo => println!("{} 还没开始", item.title),
        WorkItemStatus::Doing { assignee } => println!("由 {assignee} 处理"),
        WorkItemStatus::Done { needs_review } => println!("复盘:{needs_review}"),
    }
}

_ 可以忽略整个值,.. 可以忽略结构中剩余字段:

rust
match item {
    WorkItem { id, title, .. } => println!("#{id} {title}"),
}

但核心业务分支不要过早使用 _,否则新增枚举变体时不会得到遗漏提醒。

模式还可组合范围、绑定与守卫:

rust
fn urgency(item: &WorkItem) -> &'static str {
    match item {
        WorkItem {
            priority: Priority::High,
            estimate_hours: hours @ 8..=u8::MAX,
            ..
        } => {
            println!("大型高优工作项:{hours} 小时");
            "立即拆分并开始"
        }
        WorkItem { estimate_hours, .. } if *estimate_hours <= 1 => "快速完成",
        _ => "正常排期",
    }
}

hours @ pattern 在匹配的同时绑定值;守卫 if ... 负责模式语法不便表达的额外条件。


六、if letmatches!:只关心一个模式

如果只关心「进行中」,无需写完整 match

rust
if let WorkItemStatus::Doing { assignee } = &item.status {
    println!("当前负责人:{assignee}");
}

如果只需要布尔值,使用 matches!

rust
let is_high = matches!(item.priority, Priority::High);
let is_active = matches!(
    &item.status,
    WorkItemStatus::Todo | WorkItemStatus::Doing { .. }
);
let owned_by_lin = matches!(
    &item.status,
    WorkItemStatus::Doing { assignee } if assignee == "Lin"
);

选择规则:

  • 需要处理所有分支或产生返回值:match
  • 只关心一个模式:if let
  • 只需要真假:matches!

如果「不匹配」也有业务意义,就不要为了少写几行而使用 if let,显式 match 更容易审查。


七、方法:把合法状态转换放回类型身边

impl 为工作项定义构造、查询与变更:

rust
impl WorkItem {
    fn new(
        id: u64,
        title: &str,
        priority: Priority,
        estimate_hours: u8,
        tags: [String; 2],
    ) -> Self {
        Self {
            id,
            title: title.to_string(),
            priority,
            status: WorkItemStatus::Todo,
            estimate_hours,
            tags,
        }
    }

    fn start(&mut self, assignee: &str) -> bool {
        match self.status {
            WorkItemStatus::Todo => {
                self.status = WorkItemStatus::Doing {
                    assignee: assignee.to_string(),
                };
                true
            }
            _ => false,
        }
    }

    fn finish(&mut self, needs_review: bool) -> bool {
        match self.status {
            WorkItemStatus::Doing { .. } => {
                self.status = WorkItemStatus::Done { needs_review };
                true
            }
            _ => false,
        }
    }

    fn status_text(&self) -> String {
        status_text(&self.status)
    }

    fn is_urgent(&self) -> bool {
        matches!(self.priority, Priority::High)
            && !matches!(&self.status, WorkItemStatus::Done { .. })
    }
}

接收者表达方法权限:

写法含义典型用途
&self只读借用查询、格式化、计算
&mut self可变借用修改字段、转换状态
self取得所有权消耗或转换成另一类型

WorkItem::new 没有 self 参数,是关联函数。new 不是关键字,只是 Rust 社区常用的构造命名。

调用主流程:

rust
fn main() {
    let mut item = WorkItem::new(
        1,
        "整理 Rust 学习笔记",
        Priority::High,
        6,
        [String::from("学习"), String::from("Rust")],
    );

    println!("{}", item.status_text());
    assert!(item.start("Lin"));
    assert!(!item.start("Kai"));
    assert!(item.finish(true));
    println!("{}", item.status_text());
}

这里用 bool 保持示例简洁。工程代码通常应使用第 6 章的 Result<T, E> 解释「为什么转换失败」。


八、常见误区

  1. 用字符串模拟有限状态"hight" 也能编译;枚举能消灭拼写分叉并支持穷尽检查。
  2. 用多个布尔字段表示互斥状态is_todois_doingis_done 可以同时为真;枚举天然保证三选一。
  3. ..old 当完整复制:非 Copy 字段可能被移动,旧值不一定还能使用。
  4. 按值匹配只读数据:枚举携带 String 时可能移动字段;观察数据时匹配 &value
  5. 核心匹配过早使用 _:会吞掉未来新增变体,失去编译器提醒。
  6. 所有逻辑都塞进 impl:方法适合类型自身行为;跨多个对象、外部服务和持久化的流程应放在更高层模块。

九、实践练习

练习一:为标识建立类型身份

定义 WorkItemId(u64)UserId(u64),让二者不能被误传。为它们派生必要的比较和调试 Trait,并写一个只接收 WorkItemId 的查询函数。

练习二:重构非法状态

先用 active: boolowner: Option<String>reviewed: bool 描述工作项,再改成携带数据的枚举。列出旧模型能构造、而新模型无法构造的矛盾组合。

练习三:完整处理状态

实现 status_text(&self) -> String,使用 match 覆盖所有状态。随后给枚举增加 Blocked { reason: String },观察编译器如何指出遗漏分支。

练习四:约束状态转换

实现 startfinish 方法,让非法转换返回 Result。不要直接公开状态字段,确保调用方只能通过方法改变状态。

练习五:选择集合类型

分别用 [String; 2]Vec<String> 表示标签,说明固定长度和动态长度对 API、验证与内存布局意味着什么。

十、总结

  • 建模应从不变量出发,而不是从字段清单出发。
  • 结构体表达多个字段同时存在,枚举表达有限状态中的互斥选择。
  • 携带数据的枚举能让每种状态只保存自己需要的信息,从结构上排除矛盾组合。
  • match 同时完成分支、解构与结果返回,并在状态扩展时帮助发现遗漏。
  • if let 适合只关心单一模式,matches! 适合得到布尔判断。
  • 方法应维护类型不变量,把合法状态转换放在数据旁边,而不是散落在调用方。
  • 好的类型设计会让错误代码难写,让正确用法自然出现。

十一、下一章

现在的工作项模型足够准确,但还缺少复用能力:工作项和里程碑如何共享行为?容器如何支持不同类型?返回引用时,编译器如何确认它不会悬垂?

下一章进入 第5章:抽象能力。


  • Rust 深度教程
  • 第3章:所有权
  • 第5章:抽象能力
  • Claude Code Harness 深度教程 —— 连续教程的结构参考
  • Pi-Agent 深度教程 —— 技术概念的递进组织参考

本章目录
一、从不变量倒推类型二、元组:轻量组合,但不要承载过多语义三、结构体与数组:描述一个完整工作项四、枚举:让无效状态不可表示五、match:判断、解构与返回值一次完成六、if let 与 matches!:只关心一个模式七、方法:把合法状态转换放回类型身边八、常见误区九、实践练习十、总结十一、下一章Related Documents
苏ICP备2025204887号-2