0


【一起学Rust | 设计模式】新类型模式

文章目录


前言

新的类型模式提供封装以及保证在编译时提供正确类型的值。新类型模式有多种用途和好处,比如可以处理不同的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设计模式中的

新类型模式

,并且指明了该设计模式的使用场景,其优点与缺点。并且通过一个实例来应用

新类型模式

,拓展包装类型的行为和特质,从而实现处理包与包之间结构体和特质的关系,限制类型内容等操作。


本文转载自: https://blog.csdn.net/weixin_47754149/article/details/126088219
版权归原作者 广龙宇 所有, 如有侵权,请联系我们删除。

“【一起学Rust | 设计模式】新类型模式”的评论:

还没有评论