本章围绕“交付工作项”建立领域模型。我们不从结构体语法开始,而是先列出必须成立的规则,再选择元组、结构体、枚举、模式匹配和方法,把规则逐步压进类型系统。
假设一个交付工作项有编号、标题、优先级和状态。状态只能在“待安排、执行中、已完成”三者中选择;执行中必须记录负责人,完成后必须记录复核结论。
如果把这些信息拆成若干布尔值和可空字段,调用者很容易组合出矛盾状态。更稳妥的做法是先写不变量:
然后再为每类关系选择工具:
| 关系 | 建模工具 |
|---|---|
| 多个字段同时构成一个对象 | 结构体 |
| 多种状态只能选择一种 | 枚举 |
| 从数据形状中取出字段 | 模式匹配 |
| 合法的状态转换 | 方法 |
| 短期返回多个值 | 元组 |
| 数量固定的同类元素 | 数组 |
建模顺序 先写“永远必须成立什么”,再写类型;不要先创建一堆字段,最后才用注释解释哪些组合不合法。
函数需要同时返回最小和最大工时,可以先用元组:
元组长度固定,各位置可以是不同类型;既能用 .0 访问,也能用模式解构。它适合短生命周期的临时组合,但 (u8, u8) 没有说明两个位置的业务含义。语义稳定后,应改成命名结构体:
元组结构体则适合给基础值增加类型身份:
WorkItemId 与普通 u64 不再是同一类型,可以减少把用户编号、订单编号和工作项编号混用的机会。
先组合「同时存在」的字段:
#[derive(Debug)] 让编译器生成调试输出能力。它适合开发期观察数据,不等于面向用户的正式展示格式。
当变量名与字段名相同,可以使用字段初始化简写:
也可以基于旧值构造新值:
..item 不是继承,也不保证完整复制。String 等非 Copy 字段可能被移动,旧值之后不能再作为完整值使用。
数组 [T; N] 把元素类型与长度同时写进类型。[String; 2] 与 [String; 3] 是不同类型,适合表达「恰好两个标签」。安全访问优先考虑 .get():
直接使用 tags[2] 越界会在运行时 panic;.get(2) 返回 Option<&String>,调用者必须处理没有值的情况。若标签数量需要动态增减,应改用 Vec<T>。
工作项状态不应只是三个字符串,因为不同状态携带的数据不同:
三个变体属于同一个 WorkItemStatus 类型,却分别携带不同数据:
这样建模后:
把状态加入工作项:
标准库的 Option<T> 也是枚举:Some(T) 表示有值,None 表示没有值。Rust 不提供可随处出现的 null,而是让「缺失」成为必须处理的类型分支。
给状态生成展示文本:
match 是表达式,每个分支产生一个值。它还要求穷尽:若新增 Cancelled,编译器会指出遗漏的匹配位置。这是枚举状态比字符串状态可靠的关键原因。
只读取携带 String 的枚举时,优先匹配引用,避免移动内部数据:
_ 可以忽略整个值,.. 可以忽略结构中剩余字段:
但核心业务分支不要过早使用 _,否则新增枚举变体时不会得到遗漏提醒。
模式还可组合范围、绑定与守卫:
hours @ pattern 在匹配的同时绑定值;守卫 if ... 负责模式语法不便表达的额外条件。
如果只关心「进行中」,无需写完整 match:
如果只需要布尔值,使用 matches!:
选择规则:
如果「不匹配」也有业务意义,就不要为了少写几行而使用 if let,显式 match 更容易审查。
用 impl 为工作项定义构造、查询与变更:
接收者表达方法权限:
| 写法 | 含义 | 典型用途 |
|---|---|---|
| &self | 只读借用 | 查询、格式化、计算 |
| &mut self | 可变借用 | 修改字段、转换状态 |
| self | 取得所有权 | 消耗或转换成另一类型 |
WorkItem::new 没有 self 参数,是关联函数。new 不是关键字,只是 Rust 社区常用的构造命名。
调用主流程:
这里用 bool 保持示例简洁。工程代码通常应使用第 6 章的 Result<T, E> 解释「为什么转换失败」。
定义 WorkItemId(u64) 与 UserId(u64),让二者不能被误传。为它们派生必要的比较和调试 Trait,并写一个只接收 WorkItemId 的查询函数。
先用 active: bool、owner: Option<String>、reviewed: bool 描述工作项,再改成携带数据的枚举。列出旧模型能构造、而新模型无法构造的矛盾组合。
实现 status_text(&self) -> String,使用 match 覆盖所有状态。随后给枚举增加 Blocked { reason: String },观察编译器如何指出遗漏分支。
实现 start 与 finish 方法,让非法转换返回 Result。不要直接公开状态字段,确保调用方只能通过方法改变状态。
分别用 [String; 2] 与 Vec<String> 表示标签,说明固定长度和动态长度对 API、验证与内存布局意味着什么。
现在的工作项模型足够准确,但还缺少复用能力:工作项和里程碑如何共享行为?容器如何支持不同类型?返回引用时,编译器如何确认它不会悬垂?
下一章进入 第5章:抽象能力。