Skip to content

Latest commit

 

History

History
98 lines (63 loc) · 2.91 KB

File metadata and controls

98 lines (63 loc) · 2.91 KB

错误设计和类型设计

Rust 的强项不是“写得少”,而是“让错误和状态变得可见”。这份文档讲的是如何用类型表达约束,而不是用注释和约定表达约束。

仓库里的核心类型

TaskId

task_domain::TaskId 是一个围绕 Uuid 的新类型:

  • 可复制、可比较、可哈希
  • 可以序列化/反序列化
  • 可以从字符串解析
  • 可以通过 Display 直接输出

这类 newtype 的价值是把“任务 ID”从普通字符串里区分出来,减少误用。

Task

task_domain::Task 表示已经验证过的任务:

  • id() -> TaskId
  • title() -> &str
  • is_completed() -> bool
  • complete() -> bool

其中 complete() 会返回一个布尔值,表示这次调用是否改变了状态。

这意味着“完成任务”是幂等的:第一次会改变状态,后续再次调用不会继续改变结果。

TaskError

task_domain::TaskError 有两个域内错误:

  • EmptyTitle
  • TitleTooLong { max, actual }

这种错误设计的好处是:

  • 调用者可以分辨失败原因
  • 测试可以直接比较枚举值
  • API 可以把域内错误稳定地映射成 HTTP 响应

先在构造阶段拦截非法输入

Task::new 会在构造时完成校验:

  • 去掉首尾空白
  • 拒绝空标题
  • 拒绝超过 MAX_TITLE_LEN 的标题

这比“先创建一个不合法对象,后面再想办法修正”更安全。

Result 是主路径,不是补丁

labs/java-to-rust/examples/errors.rs 里的模式如下:

fn describe_creation(title: &str) -> Result<String, TaskError> {
    let task = Task::new(title)?;
    Ok(format!("created {} ({})", task.title(), task.id()))
}

这说明:

  • 函数签名直接暴露失败可能性
  • ? 负责把错误沿调用链向上传递
  • 成功和失败都被显式建模

API 层如何把错误转成 HTTP

apps/task-api 里的错误响应是结构化的,不是裸字符串:

情况 HTTP 状态 响应 code 响应 message
标题非法 422 Unprocessable Entity invalid_task 来自 TaskError::to_string()
找不到任务 404 Not Found task_not_found task {id} does not exist

这类设计的目标是:前端或 CLI 可以稳定处理错误,而不是解析一段不稳定的文本。

类型设计的实用规则

  1. 用 newtype 保护关键 ID
  2. 用枚举表达有限错误集合
  3. Result 表达会失败的构造或操作
  4. 用方法代替直接暴露内部字段
  5. 让“合法状态”比“非法状态”更容易构造

不要回到 Java 的默认异常思维

Java 里常见的做法是“先 new 出来,再靠运行时异常兜底”。Rust 更像是:

  • 先把类型设计对
  • 再让非法输入在入口处失败
  • 再把错误作为数据向上传递

如果你把 Rust 的错误处理继续写成“到处抛异常”的样子,你会错过 Rust 最有价值的部分。