Rust如何定义仅接受A-Z字符子集的自定义char类型
Rust 原生的char类型覆盖所有Unicode标量值,合法取值范围包含上百万个可能值,编译器无法识别代码注释里“仅存储A-Z”的手动约束,因此会触发E0004错误,要求补充通配匹配臂覆盖其余所有可能的取值。
要实现无通配臂的穷尽匹配、同时从类型层面强制取值只能是大写A-Z,有两种成熟实现方案:
枚举的所有变体对编译器完全可见,只要覆盖全部变体就会被判定为匹配穷尽,是最符合需求的方案。
我们可以给枚举加#[repr(u8)]标记,指定判别值和大写字母的ASCII码一一对应,简化转换逻辑:
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum UpperChar { A = b'A', B = b'B', C = b'C', D = b'D', E = b'E', F = b'F', G = b'G', H = b'H', I = b'I', J = b'J', K = b'K', L = b'L', M = b'M', N = b'N', O = b'O', P = b'P', Q = b'Q', R = b'R', S = b'S', T = b'T', U = b'U', V = b'V', W = b'W', X = b'X', Y = b'Y', Z = b'Z' }
为枚举实现基础的转换、旋转方法:
impl UpperChar { /// 从普通char构造UpperChar,输入非大写A-Z时返回错误 pub fn from_char(c: char) -> Result<Self, char> { match c { 'A'..='Z' => { // 安全性:枚举判别值和A-Z的ASCII码一一对应连续排列,transmute完全合法 Ok(unsafe { std::mem::transmute(c as u8) }) } invalid => Err(invalid), } } /// 转换为普通char类型 pub fn as_char(self) -> char { self as u8 as char } /// 保留原有match写法的旋转实现,不需要通配臂 pub fn rotate(&mut self) { match *self { UpperChar::A..=UpperChar::Y => { // 安全性:当前值在A-Y范围,+1后仍在合法判别值范围内 *self = unsafe { std::mem::transmute(*self as u8 + 1) } } UpperChar::Z => *self = UpperChar::A, } } } // 实现Step trait以支持枚举的范围模式匹配 impl std::iter::Step for UpperChar { fn steps_between(start: &Self, end: &Self) -> Option<usize> { Some((*end as u8 - *start as u8) as usize) } fn forward_checked(start: Self, count: usize) -> Option<Self> { let res = start as u8 as usize + count; (res <= b'Z' as usize).then(|| unsafe { std::mem::transmute(res as u8) }) } fn backward_checked(start: Self, count: usize) -> Option<Self> { let res = start as u8 as usize - count; (res >= b'A' as usize).then(|| unsafe { std::mem::transmute(res as u8) }) } }
之后修改原有的Rotor结构体,把position、notch字段的char类型替换为UpperChar即可,所有匹配逻辑不需要加通配臂,编译器会自动校验匹配是否穷尽。
如果觉得枚举写26个变体太繁琐,可以用newtype模式包装u8,把所有值校验、修改逻辑都封装在类型内部,对外不暴露内部值,从运行时层面保证值永远在A-Z范围内:
#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct UpperChar(u8); // 内部约定:存储值永远是b'A'到b'Z'的ASCII码 impl UpperChar { /// 从普通char构造UpperChar,输入非大写A-Z时返回错误 pub fn from_char(c: char) -> Result<Self, char> { c.is_ascii_uppercase() .then_some(Self(c as u8)) .ok_or(c) } /// 转换为普通char类型 pub fn as_char(self) -> char { self.0 as char } /// 旋转逻辑完全封装在内部,外部不需要直接匹配值 pub fn rotate(&mut self) { self.0 = (self.0 - b'A' + 1) % 26 + b'A'; } }
你之前提到的const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";可以直接用在构造方法的校验逻辑里,替换c.is_ascii_uppercase()判断即可,比如CHARSET.contains(&(c as u8)),效果完全一致。
这种方案不需要写枚举变体,也没有unsafe代码,唯一的区别是编译器无法感知内部值的范围约束,但只要你把所有涉及值修改、匹配的逻辑都封装在impl块内部,外部根本不需要直接接触内部值,也就不需要写妥协的通配臂。
内容的提问来源于stack exchange,提问作者Brandon Piña

