文章目录
前言
新的类型模式提供封装以及保证在编译时提供正确类型的值。新类型模式有多种用途和好处,比如可以处理不同的crate中的结构体和特质的关系。本期我们将一起探讨一下Rust设计模式中的新类型模式。
本期内容是学习Rust设计模式笔记
一、新类型模式
如果在某些情况下,我们希望一个类型的行为类似于另一种类型,或者在编译时强制执行某些行为,而实现这些仅使用类型别名是不够的。
例如,出于安全考虑 ,我们想要为String创建自定义实现。对于这种情况,我们可以使用该Newtype模式来提供
安全类型
和
封装
。
1. 新类型模式的实现
使用单个字段的元组来实现包装一个类型,使之称为一个新类型,而不是那个类型的别名,这样就可以拓展这个类型。
2. 官方例子
下面这段代码来自《Rust设计模式》,只是伪代码,并未实现具体功能,但是描述了新类型模式的思想:使用元组来包装一个新类型,通过拓展这个新类型,来拓展原本类型的功能。Foo是一个基础类型,他有他本身的实现方法,Bar包装自Foo,它除了有Foo的特性,还为其实现了新的方法,通过测试,这两种类型,已然变成了两种独立的类型。
// 一个类型,可以是自己包里面的,也可以是别的包里面的。structFoo{//..}implFoo{// Foo 类型的实现,这些实现在Bar里面是没有的,Bar是下面的类型。//..}// 一个新类型,它包装自FoopubstructBar(Foo);implBar{// 构造函数.pubfnnew(//..)->Self{//..}//..}fnmain(){let b =Bar::new(...);// Foo和Bar类型不兼容,以下不进行类型检查。// let f: Foo = b;// let b: Bar = Foo { ... };}
3. 使用动机
新类型的主要动机是抽象。它允许在精确控制接口的同时在类型之间共享实现细节。通过使用新类型而不是将实现类型作为 API 的一部分公开,它允许您向后兼容地更改实现。
这样就是区分同一个类型不同含义,就比如
f64
类型,可以定义成米,或者千米等不同的类型。
4. 优点
新类型是一种零成本的抽象——没有运行时开销。
新类型与包装类型是互不兼容的,因此用户不会混淆两种类型。
Rust的隐私系统确保用户不会访问到包装了的类型,他的字段默认是私有的。
5. 缺点
没有语言支持,因此包装类型的每一个方法都必须写一个传递方法,来使用包装类型的方法。并且要为每一个实现了的特质来写传递方法。这样就显得极为复杂。
二、应用
1.标识符分离
假设我们有一个用户id,他是无符号整数类型的
usize
。我们系统对用户操作都是通过这个用户id来实现的。就比如我们有这么一个方法:
fnget_user_id_from_username(username:&str)->usize
如果我们还要对用户的帖子进行操作,那么代码应该这么写
let user_id:usize=get_user_id_from_username(username);let post_id:usize=get_last_post();fndelete_post(post_id:usize){// ...}delete_post(user_id);
此时我们使用
delete_post
删除用户帖子,但是不小心传入了用户id,这就很麻烦了,这样辨识度就很不好,为了提高辨识度,我们使用
新类型模式
来区分两个类型:
structUserId(pubusize);
然后让
get_user_id_from_username
返回该类型,
fnget_user_id_from_username(username:String)->UserId{let user_id:usize=...UserId(user_id)}
这里做了如下改动
这样在我们写错代码的时候就会这么提示了
新类型模式在编译时强制执行类型安全,而在运行时没有任何性能开销。
2.为新类型添加功能
现在有如下需求,需要为用户设置个“禁止登录”的用户列表。考虑使用
HashSet
实现,我们定义的代码如下
let banned_users:HashSet<UserId>=HashSet::new();
但是光这一点是无法编译的,我们的UserId并没有想等,哈希等行为。我们可以使用内置派生宏来实现这些行为,会自动基于我们的结构体来生成这些实现,代码如下,
#[derive(PartialEq, Eq, Hash)]structUserId(usize);
3. 限制类型内容
有时候我们需要对用户名进行校验,比如我们需要用户名全部都是由小写字母组成的。现在我们来将String类型来定义成一个新类型,Username
structUsername(String);
然后创建个创建用户名的唯一方法,我们使用TryFrom特质
implTryFrom<String>forUsername{typeError=String;fntry_from(value:String)->Result<Self,Self::Error>{if value.chars().all([object Object]c[object Object]matches!(c,'a'..='z')){Ok(Username(value))}else{Err(value)}}}
这里重写了try_from行为,在类型转换时就已经对username进行了检测构造符合条件的用户名
4. 处理包之间特质和结构体的关系
在使用外部特质时我们可能会遇到以下问题
在我们的crate中使用MyTrait时,编译器就不知道我们用的是crate3中的MyTrait还是crate4中的MyTrait。Rust有一套《孤儿规则》专门来处理这种情况,我们会在后期的文章中说明。
现在我们使用
新类型模式
来实现外来结构体特质,或者拓展特质。
在某crate包中有如下特质
traitToTree{// ...}fnvery_useful_function(something:implToTree)->(){// ..}
在我们的crate中这么写
structWrapper(pub crate_y[object Object]MyType);implToTreeforWrapper{// ...}// 使用very_useful_function(Wrapper(foreign_value))
总结
本期介绍了Rust设计模式中的
新类型模式
,并且指明了该设计模式的使用场景,其优点与缺点。并且通过一个实例来应用
新类型模式
,拓展包装类型的行为和特质,从而实现处理包与包之间结构体和特质的关系,限制类型内容等操作。
版权归原作者 广龙宇 所有, 如有侵权,请联系我们删除。