草庐IT

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

广龙宇 2023-04-14 原文

文章目录


前言

新的类型模式提供封装以及保证在编译时提供正确类型的值。新类型模式有多种用途和好处,比如可以处理不同的crate中的结构体和特质的关系。本期我们将一起探讨一下Rust设计模式中的新类型模式。

本期内容是学习Rust设计模式笔记


一、新类型模式

如果在某些情况下,我们希望一个类型的行为类似于另一种类型,或者在编译时强制执行某些行为,而实现这些仅使用类型别名是不够的。
例如,出于安全考虑 ,我们想要为String创建自定义实现。对于这种情况,我们可以使用该Newtype模式来提供安全类型封装

1. 新类型模式的实现

使用单个字段的元组来实现包装一个类型,使之称为一个新类型,而不是那个类型的别名,这样就可以拓展这个类型。

2. 官方例子

下面这段代码来自《Rust设计模式》,只是伪代码,并未实现具体功能,但是描述了新类型模式的思想:使用元组来包装一个新类型,通过拓展这个新类型,来拓展原本类型的功能。Foo是一个基础类型,他有他本身的实现方法,Bar包装自Foo,它除了有Foo的特性,还为其实现了新的方法,通过测试,这两种类型,已然变成了两种独立的类型。

// 一个类型,可以是自己包里面的,也可以是别的包里面的。
struct Foo {
    //..
}

impl Foo {
    // Foo 类型的实现,这些实现在Bar里面是没有的,Bar是下面的类型。
    //..
}

// 一个新类型,它包装自Foo
pub struct Bar(Foo);

impl Bar {
    // 构造函数.
    pub fn new(
        //..
    ) -> Self {

        //..

    }

    //..
}

fn main() {
    let b = Bar::new(...);

    // Foo和Bar类型不兼容,以下不进行类型检查。
    // let f: Foo = b;
    // let b: Bar = Foo { ... };
}

3. 使用动机

新类型的主要动机是抽象。它允许在精确控制接口的同时在类型之间共享实现细节。通过使用新类型而不是将实现类型作为 API 的一部分公开,它允许您向后兼容地更改实现。

这样就是区分同一个类型不同含义,就比如f64类型,可以定义成米,或者千米等不同的类型。

4. 优点

新类型是一种零成本的抽象——没有运行时开销。

新类型与包装类型是互不兼容的,因此用户不会混淆两种类型。

Rust的隐私系统确保用户不会访问到包装了的类型,他的字段默认是私有的。

5. 缺点

没有语言支持,因此包装类型的每一个方法都必须写一个传递方法,来使用包装类型的方法。并且要为每一个实现了的特质来写传递方法。这样就显得极为复杂。

二、应用

1.标识符分离

假设我们有一个用户id,他是无符号整数类型的usize。我们系统对用户操作都是通过这个用户id来实现的。就比如我们有这么一个方法:

fn get_user_id_from_username(username: &str) -> usize

如果我们还要对用户的帖子进行操作,那么代码应该这么写

let user_id: usize = get_user_id_from_username(username);
let post_id: usize = get_last_post();

fn delete_post(post_id: usize) {
	// ...
}

delete_post(user_id);

此时我们使用delete_post删除用户帖子,但是不小心传入了用户id,这就很麻烦了,这样辨识度就很不好,为了提高辨识度,我们使用新类型模式来区分两个类型:

struct UserId(pub usize);

然后让get_user_id_from_username返回该类型,

fn get_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)]
struct UserId(usize);

3. 限制类型内容

有时候我们需要对用户名进行校验,比如我们需要用户名全部都是由小写字母组成的。现在我们来将String类型来定义成一个新类型,Username

struct Username(String);

然后创建个创建用户名的唯一方法,我们使用TryFrom特质

impl TryFrom<String> for Username {
	type Error = String;

	fn try_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包中有如下特质

trait ToTree {
   // ...
}

fn very_useful_function(something: impl ToTree) -> () {
    // ..
}

在我们的crate中这么写

struct Wrapper(pub crate_y[object Object]MyType);

impl ToTree for Wrapper {
	// ...
}

// 使用
very_useful_function(Wrapper(foreign_value))

总结

本期介绍了Rust设计模式中的新类型模式,并且指明了该设计模式的使用场景,其优点与缺点。并且通过一个实例来应用新类型模式,拓展包装类型的行为和特质,从而实现处理包与包之间结构体和特质的关系,限制类型内容等操作。

有关【一起学Rust | 设计模式】新类型模式的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  3. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  4. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  5. ruby - Infinity 和 NaN 的类型是什么? - 2

    我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串

  6. ruby - 检查方法参数的类型 - 2

    我不确定传递给方法的对象的类型是否正确。我可能会将一个字符串传递给一个只能处理整数的函数。某种运行时保证怎么样?我看不到比以下更好的选择:defsomeFixNumMangler(input)raise"wrongtype:integerrequired"unlessinput.class==FixNumother_stuffend有更好的选择吗? 最佳答案 使用Kernel#Integer在使用之前转换输入的方法。当无法以任何合理的方式将输入转换为整数时,它将引发ArgumentError。defmy_method(number)

  7. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  8. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  9. ruby-on-rails - 如果我将 ruby​​ 版本 2.5.1 与 rails 版本 2.3.18 一起使用会怎样? - 2

    如果我使用ruby​​版本2.5.1和Rails版本2.3.18会怎样?我有基于rails2.3.18和ruby​​1.9.2p320构建的rails应用程序,我只想升级ruby的版本,而不是rails,这可能吗?我必须面对哪些挑战? 最佳答案 GitHub维护apublicfork它有针对旧Rails版本的分支,有各种变化,它们一直在运行。有一段时间,他们在较新的Ruby版本上运行较旧的Rails版本,而不是最初支持的版本,因此您可能会发现一些关于需要向后移植的有用提示。不过,他们现在已经有几年没有使用2.3了,所以充其量只能让更

  10. ruby - 查找字符串中的内容类型(数字、日期、时间、字符串等) - 2

    我正在尝试解析一个CSV文件并使用SQL命令自动为其创建一个表。CSV中的第一行给出了列标题。但我需要推断每个列的类型。Ruby中是否有任何函数可以找到每个字段中内容的类型。例如,CSV行:"12012","Test","1233.22","12:21:22","10/10/2009"应该产生像这样的类型['integer','string','float','time','date']谢谢! 最佳答案 require'time'defto_something(str)if(num=Integer(str)rescueFloat(s

随机推荐