Comprehensive Rust 中的 Token Types用私有构造器把不变量变成编译期证明【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust在 Google Android 团队的 Rust 课程Comprehensive Rust中Token Types令牌类型是「充分利用类型系统」章节的核心技法之一本指南以 token-types.md 为主线结合仓库中的权限令牌、Mutex Guard 与 Branded Types品牌化类型系列讲义系统讲解如何用私有构造器 模块边界构造出调用方无法伪造的证明型类型。读完本文你将掌握 Token 的基本建模、带数据的 TokenMutexGuard范式、用PhantomData与生命周期变型把 Token 绑定到特定变量的进阶技巧以及如何规避破坏该模式的常见陷阱。一、为什么需要 Token Types用类型表达前提已满足课程原文开门见山给出定义拥有私有构造器的类型可以被用作不变量invariant的证明Types with private constructors can be used to act as proof of invariants。其动机非常具体我们希望限制用户对某些功能的访问直到他们完成某个特定任务为止。传统做法是在每个函数入口写一堆 if/else 检查但检查极易被遗漏Token 类型把检查通过这个事实直接编码进类型系统——调用方手里没有Token就无法在编译期调用受保护的功能。这一思想与同章节的 Newtype Pattern 一脉相承newtype 利用结构体的隐私规则只在值能保证于运行时维持某个不变量时才允许构造Token 类型则把同样的隐私规则用于授予访问权这一场景。二、最小实现模块边界 私有字段课程给出的最小示例是完整可运行的其中proof字段是点睛之笔pub mod token { // A public type with private fields behind a module boundary. pub struct Token { proof: () } pub fn get_token() - OptionToken { Some(Token { proof: () }) } } pub fn protected_work(token: token::Token) { println!(We have a token, so we can make assumptions.) } fn main() { if let Some(token) token::get_token() { // We have a token, so we can do this work. protected_work(token); } else { // We could not get a token, so we cant call protected_work. } }2.1proof: ()字段为什么必不可少这是课堂上必问的问题。如果Token没有任何私有字段那么它是一个空结构体外部用户就可以直接写出Token {}任意构造它——证明就失去了意义。正是这个类型为()的私有字段proof让结构体至少有一个私有字段外部模块无法按字面量构造字段proof的可见性默认为私有只有token模块内部能访问而Token结构体本身是pub类型可以导出值却无法伪造于是谁能持有Token完全由 API 开发者定义的生产函数这里是get_token说了算。配套的教学演示步骤是先在main中尝试手动构造Token观察编译错误再把proof字段删掉观察用户如何能随意构造Token。仓库源码见 token-types.md。2.2 谁有权发 Token谁没有把Token放在token模块边界之后模块外的用户没有访问proof字段的权限因此无法自行构造该值。API 开发者可以定义产生这些 Token 的方法与函数用户不可以。由此Token 成为用户已满足 API 开发者访问条件的证明——只要你能把 Token 作为参数传进去编译器就已经替你验证过前提条件函数体内部可以放心地基于假设开展工作。三、进阶一权限令牌Permission TokensToken 类型非常适合作为已检查权限的证明Token types work well as a proof of checked permission讲义 permission-tokens.md 用一个聊天客户端的例子演示用户输入密码换取AdminToken拿到令牌后才能把人提升为管理员。mod admin { pub struct AdminToken(()); pub fn get_admin(password: str) - OptionAdminToken { if password Password123 { Some(AdminToken(())) } else { None } } } // We dont have to check that we have permissions, because // the AdminToken argument is equivalent to such a check. pub fn add_moderator(_: admin::AdminToken, user: str) {} fn main() { if let Some(token) admin::get_admin(Password123) { add_moderator(token, CoolUser); } else { eprintln!(Incorrect password! Could not prove privileges.) } }这段代码值得注意的几个设计点AdminToken(())是元组结构体内部同样只有一个()类型的私有字段作用与上一节的proof: ()完全相同——阻断外部构造add_moderator的 Token 参数以admin::AdminToken传入函数体完全不需要再次检查权限因为根本没有 Token 就调不到这个函数所以只要能调用就说明调用者必然持有权限密码校验失败时返回Nonemain中通过if let分支处理无法证明特权的情况教学演示重点仍是试图在main里直接构造AdminToken再次印证阻止任意构造是 Token 有用的根基。四、进阶二带数据的 Token——MutexGuard 范式有时 Token 类型还需要携带额外数据。mutex-guard.md 指出互斥锁守卫MutexGuard就是权限 数据结合的 Token 实例。use std::sync::{Arc, Mutex, MutexGuard}; fn main() { let mutex Arc::new(Mutex::new(42)); let try_mutex_guard: ResultMutexGuard_, _, _ mutex.lock(); if let Ok(mut guarded) try_mutex_guard { // The acquired MutexGuard is proof of exclusive access. *guarded 451; } }4.1 MutexGuard 如何同时充当证明与数据通道MutexGuard是由Mutex生成的、证明你在当前时间点拥有读写访问权的值它还持有生成它的Mutex的引用并通过Deref/DerefMut实现暴露Mutex内部的数据——底层Mutex对用户保持数据私有只有拿到守卫才能触碰数据如果mutex.lock()没有返回MutexGuard你不仅没有权限也完全没有途径访问互斥锁中的数据这与 C 形成鲜明对比C 的互斥锁与锁守卫并不控制对数据本身的访问只是一面用户每次读写数据都必须记得去检查的旗子。课程还建议演示把mutex变量改为mut后尝试直接解引用修改值会发现Mutex本身没有Deref实现除了获取守卫之外没有其他途径接触数据。这里可以与仓库中 RAII 章节的 mutex.md 对照阅读——RAII 部分侧重离开作用域自动解锁而 Token 视角则补充了守卫同时控制数据访问权这层含义。五、进阶三Branded Types——把 Token 绑定到特定变量前述 Token 只能证明某种权限已获取但无法区分这个 Token 属于哪个具体变量。比如我们想要一个已知合法下标的 Token对数组 A 校验得到的合法下标如果错误地用到数组 B 上就可能越界。Branded Types品牌化类型系列branded-01-motivation.md 至 branded-04-in-action.md解决了这个问题思路是用生命周期作为每个 Token 的独特品牌brand。5.1 动机下标串用是未定义行为首先看未品牌化的版本它有一个隐蔽的 bugstruct Bytes { bytes: Vecu8, } struct ProvenIndex(usize); impl Bytes { fn get_index(self, ix: usize) - OptionProvenIndex { if ix self.bytes.len() { Some(ProvenIndex(ix)) } else { None } } fn get_proven(self, token: ProvenIndex) - u8 { unsafe { *self.bytes.get_unchecked(token.0) } } } fn main() { let data_1 Bytes { bytes: vec![0, 1, 2] }; if let Some(token_1) data_1.get_index(2) { data_1.get_proven(token_1); // Works fine! // let data_2 Bytes { bytes: vec![0, 1] }; // data_2.get_proven(token_1); // Panics! Can we prevent this? } }动机是一旦拿到已证明存在的下标 Tokenget_proven就可以跳过边界检查此处用get_unchecked。但没有任何机制阻止data_1的 Token 被用到data_2上如果越界get_unchecked就是未定义行为。课程演示会取消data_2.get_proven(token_1)的注释程序直接 panic——而我们希望在编译期就拒绝这种跨变量串用。5.2 核心机制PhantomData 生命周期不变性Invariance品牌化的关键代码在 branded-02-phantomdata.mduse std::marker::PhantomData; #[derive(Default)] struct InvariantLifetimeid(PhantomDataid ()); // The main focus struct Wrappera { value: u8, invariant: InvariantLifetimea } fn lifetime_separatorT(value: u8, f: impl fora FnOnce(Wrappera) - T) - T { f(Wrapper { value, invariant: InvariantLifetime::default() }) } fn try_coerce_lifetimesa(left: Wrappera, right: Wrappera) {} fn main() { lifetime_separator(1, |wrapped_1| { lifetime_separator(2, |wrapped_2| { // We want this to NOT compile try_coerce_lifetimes(wrapped_1, wrapped_2); }); }); }两个关键部件缺一不可fora高阶 trait boundHRTB。它向函数类型引入一个生命周期泛型参数并要求函数体对所有可能的生命周期都成立。这剥夺了编译器对这个生命周期做特定假设的能力——调用方可以代入真实生命周期函数本身不能。它类似数学中的全称量词∀也类似T类型变量只不过作用在生命周期上。更重要的是它阻止 API 使用者自己定义生命周期否则用户就能绕过我们想施加的限制。PhantomData与生命周期变型Variance。Rust 只对生命周期存在子类型关系一个生命周期outlives另一个时前者可以视为后者的子类型两个不同的生命周期可以在重叠区域内被当作同一个使用。这正是我们要阻止的——目标是有两个编译器无法判定谁更长的生命周期。PhantomDataid ()对生命周期是协变covariant的太宽松所以上面的代码能通过编译。课程的限制阶梯演示依次尝试PhantomData中的引用类型生命周期变型类型变型能否阻止合并id ()协变协变否id mut ()协变不变否*mut id mut ()不变不变是*mut id ()不变协变是最终答案是把生命周期放在可变裸指针*mut之后即PhantomData*mut id ()借用检查器无法在安全 Rust 中推理可变裸指针于是编译器对id只能知道a的子类型只有a本身任何两个由lifetime_separator产生的品牌化值都无法互相子类型化try_coerce_lifetimes(wrapped_1, wrapped_2)就会编译失败。5.3 实现品牌化类型为什么new不返回Bytesbranded-03-impl.md 给出了完整实现use std::marker::PhantomData; #[derive(Default)] struct InvariantLifetimeid(PhantomData*mut id ()); struct ProvenIndexid(usize, InvariantLifetimeid); struct Bytesid(Vecu8, InvariantLifetimeid); implid Bytesid { fn newT( // The data we want to modify in this context. bytes: Vecu8, // The function that uniquely brands the lifetime of a Bytes f: impl fora FnOnce(Bytesa) - T, ) - T { f(Bytes(bytes, InvariantLifetime::default()),) } fn get_index(self, ix: usize) - OptionProvenIndexid { if ix self.0.len() { Some(ProvenIndex(ix, InvariantLifetime::default())) } else { None } } fn get_proven(self, ix: ProvenIndexid) - u8 { debug_assert!(ix.0 self.0.len()); unsafe { *self.0.get_unchecked(ix.0) } } }这里有三个值得深究的设计决策new不返回Bytes而是接收一个一次性闭包。因为我们需要Bytes拥有一个由 API 控制的唯一生命周期。假如new()的签名是fn newa() - BytesaAPI 用户就能自行选择a从而破坏不同实例生命周期唯一、无法互相子类型化的保证。闭包 fora的形态把品牌化生命周期的决定权完全收回 API 内部。get_index与get_proven缺一不可。下标是否合法只能在运行时知道get_index做检查但这个合法下标属于哪个Bytes可以在编译期保证ProvenIndexid与Bytesid共享同一品牌。重点不仅是省掉边界检查更是杜绝下标的跨变量串用。生产环境下get_proven建议保留debug_assert!作为双保险讲义版本用unsafedebug_assertin action一节的简化版本直接用self.0[ix.0]索引。5.4 实战效果与扩展方向branded-04-in-action.md 展示了最终的嵌套调用形态fn main() { let result Bytes::new(vec![4, 5, 1], |mut bytes_1| { Bytes::new(vec![4, 2], |mut bytes_2| { let index_1 bytes_1.get_index(2).unwrap(); let index_2 bytes_2.get_index(1).unwrap(); bytes_1.get_proven(index_1); bytes_2.get_proven(index_2); // bytes_2.get_proven(index_1); // ❌ Computations done! }) }); println!({result}); }bytes_2.get_proven(index_1)一旦取消注释即编译失败——两个Bytes实例的品牌生命周期不同Token 无法跨变量使用。这是限制性极强但换来的安全性极有意义的典型它把过去只能靠运行时检查且只能保证 panic、不能预防错误本身的问题提升到了编译期拒绝的高度。在此基础上可以自然延伸push操作是能保证产生合法下标的操作可以返回ProvenIndexidBytes可以泛化为BrandedVecid, T包装任意VecT。讲义指出GhostCell用于在 Rust 中安全实现循环数据结构等正是这类品牌化 Token 的著名实践者其论文中的BrandedVec实现即为本系列幻灯片的基础。六、常见陷阱别亲手拆掉不可构造的防线课程在 token-types.md 的讨论环节提出一个关键问题API 开发者可能无意中引入哪些绕过机制答案是三类典型实现序列化实现serialization implementations如果为 Token 实现了反序列化如serde的Deserialize外部输入就能直接构造 Token绕过权限检查解析器 /FromStr实现任何从字符串解析出 Token的入口都是后门Default的实现一旦Default被实现Token::default()就能凭空构造 Token。设计 Token 类型时应保持构造入口的最小化 受控除了 API 自己提供的受信生产函数其余 trait 实现尤其是反序列化与Default都要三思而后行。这与 Newtype 模式的原则一致newtype 默认不携带任何行为你需要刻意决定转发底层类型的哪些方法、实现哪些 trait例如UserId允许比较但通常不应允许加减运算。七、小结把运行时检查前移到编译期证明回看本系列在课程中的位置它是「充分利用类型系统」章节的一部分与 Newtype、借用检查不变量、Typestate 模式等共同构成用类型使代码更难被误用的武器库。Token Types 提供的是一条完整的能力阶梯基础 Token私有构造器 模块边界证明某种前提条件已满足如权限已获取带数据的 TokenMutexGuard范式证明 数据通道二合一品牌化 TokenPhantomData*mut id ()的不变生命周期 foraHRTB把 Token 精确绑定到某个变量实例。三者的共同根基是同一个洞察只要调用方无法自行构造 Token那么能够调用受保护函数这件事本身就是编译器替你完成的权限检查。用类型系统表达不变量往往能以零运行时开销换取更高层次的安全保证——这正是 Rust 类型系统表达力的精髓所在。延伸阅读仓库内Newtype PatternToken 依赖的隐私规则的另一面Permission Tokens聊天客户端的权限证明实例Token Types with Data: Mutex Guards权限 数据组合Branded Types 系列生命周期品牌化的完整四讲RAII 章节的 Mutex 讲义互斥锁自动解锁的配套内容Leveraging the Type System本节在整门课程中的定位【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
