功能说明
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));
}