← 中文资源

简体中文版本

用 Newtype 包装校验类型化 ID

用 newtype 封装数字 ID,解析固定前缀、拒绝零值并输出补零后的规范形式。

阅读英文原文

功能说明

Rust 的 newtype 模式用一个只含单个字段的元组结构体包装基础类型,从而在类型系统中区分“用户 ID”和“订单 ID”,避免把两者混用。这段代码在包装的基础上加入解析与校验:只接受 usr_ 前缀、纯数字且大于零的字符串,并用固定宽度输出规范形式。

它适合领域模型、API 参数解析和数据库主键映射。把校验放进 FromStr 后,任何从字符串构造 UserId 的路径都必须经过同一套规则,减少遗漏。

关键代码

UserId(u64) 是私有字段的元组结构体,外部模块不能直接构造任意值。impl FromStr 先调用 strip_prefix("usr_"),缺失前缀返回 IdError::Prefix;空数字返回 Empty;parse::<u64> 失败返回 Digits;值为零返回 Zero。

Display 用 write!(formatter, "usr_{:06}", self.0) 输出至少六位数字,不足补零。main 验证正常解析、补零输出以及三类错误。

使用注意

补零只影响显示,usr_000001 与 usr_1 解析后是同一个值;如果业务要求保留原始写法,需要额外字段。解析接受前导零,但不接受正负号、空白或下划线分隔。

不要把内部 u64 通过 Deref 或 pub 字段暴露出去,否则调用方可以绕过校验构造零值。与其他 ID 类型之间不要实现隐式转换,需要时显式提供方法。错误类型实现 Display 和 std::error::Error 后更容易与上层错误组合。

验证方式

示例把 usr_004210 解析为 UserId(4210),输出仍为 usr_004210。usr_000000 返回 Zero,4210 返回 Prefix,usr_12x 返回 Digits,三个断言都通过。可以再加入超出 u64 范围的数字,确认会落到 Digits 错误。

代码示例

use std::fmt;
use std::str::FromStr;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct UserId(u64);

#[derive(Debug, PartialEq, Eq)]
enum IdError {
    Empty,
    Prefix,
    Digits,
    Zero,
}

impl fmt::Display for IdError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let message = match self {
            IdError::Empty => "identifier digits are empty",
            IdError::Prefix => "identifier must start with usr_",
            IdError::Digits => "identifier must contain only digits",
            IdError::Zero => "identifier must be greater than zero",
        };
        formatter.write_str(message)
    }
}

impl FromStr for UserId {
    type Err = IdError;

    fn from_str(raw: &str) -> Result<Self, Self::Err> {
        let digits = raw.strip_prefix("usr_").ok_or(IdError::Prefix)?;
        if digits.is_empty() {
            return Err(IdError::Empty);
        }
        let value: u64 = digits.parse().map_err(|_| IdError::Digits)?;
        if value == 0 {
            return Err(IdError::Zero);
        }
        Ok(UserId(value))
    }
}

impl fmt::Display for UserId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "usr_{:06}", self.0)
    }
}

fn main() {
    let id: UserId = "usr_004210".parse().expect("valid id");
    println!("{id}");
    assert_eq!(id.to_string(), "usr_004210");
    assert_eq!("usr_000000".parse::<UserId>(), Err(IdError::Zero));
    assert_eq!("4210".parse::<UserId>(), Err(IdError::Prefix));
    assert_eq!("usr_12x".parse::<UserId>(), Err(IdError::Digits));
}