5 traits your Rust types must implement

https://www.youtube.com/watch?v=Nzclc6MswaI

五个你的Rust类型绝对应该实现的标准库特性

特别是如果你要将你的类型作为公共接口的一部分来暴露。你看,Rust的孤儿规则规定你不能为外部类型实现外部特性。这意味着你的库的用户将无法自己实现这些特性,除非他们创建一个包装类型,这是很烦人的。而且,如果没有访问类型的内部结构,他们可能甚至无法写出合理的实现。你最不希望看到的就是有人尝试使用你的Rust库然后放弃。这五个特性提供了Rust使用者期望你的类型具有的基本功能,幸运的是,它们都可以被派生。在视频的最后,我会介绍第六个你的类型绝对应该实现的特性,它不是标准库的一部分,所以请一定要坚持到最后。

在我们开始之前,请确保通过访问letsgetrusty.com/cheatsheet获取你的免费Rust备忘单。

假设我们正在创建一个Rust库,暴露了一个名为user的pub类型。user是一个结构体,有三个字段:id、name和role。在main函数中,让我们模拟这个类型可能被我们库的用户如何使用。

首先,我们会创建一个user类型,然后用debug格式打印出来。debug格式可能非常有用,例如,假设发生了一个错误,我们想用debug格式打印出这个错误,同时也打印出user结构体以提供一些额外的上下文。

不幸的是,我们遇到了一个编译时错误。user无法使用debug格式打印,因为它没有实现Debug特性。错误信息建议我们派生这个特性,这正是我们要做的。我们还需要为role枚举实现Debug特性。

库用户可能想要做的下一件事是复制你的类型。这可以通过Clone方法来完成。

这里我们遇到了一个编译时错误,表示在user类型上没有定义名为clone的方法。这是因为我们需要派生Clone特性。

库用户可能想要做的第三件事是用默认值创建你的类型的实例。这可以通过default关联函数来完成。

就像clone一样,为了使这个函数工作,我们需要派生Default特性。

当我们派生Default特性时,对于存在合理默认值的类型会给出默认值。例如,一个无符号32位整数会默认为零,一个字符串类型会默认为空字符串。对于枚举,不存在合理的默认值,所以我们必须使用属性指定一个。在这个例子中,我们说guest变体是默认的。你也可以有一个自定义的default实现,例如,也许你不想让id默认为零或name默认为空字符串。但在我们的例子中,因为默认的role是guest,这些值是可以的。

库用户可能想要做的下一件事是比较给定类型的实例。

这在编写测试时特别有用。为了使这个功能工作,我们需要实现PartialEq特性。

这给了我们比较这个类型实例的基本能力。然而,还有一些相关的特性可以给你更多的功能。这些是PartialOrd、Hash、Eq和Ord。查看这些特性的文档,看看它们是否适用于你的用例。

你的类型应该实现的下一组特性是Send和Sync。如果你的类型可以安全地在线程之间发送,就实现了Send。如果你的类型可以通过引用安全地在线程之间共享,就实现了Sync。好消息是,这些是自动特性,这意味着它们会自动为你的类型实现。但有一个注意事项:这些特性是自动实现的,除非你的类型包含一个不是Send或Sync的值。例如,假设我们给user添加了一个新字段,它持有一个引用计数类型。

这里我们有一个名为db的字段,它持有一个数据库实例的共享引用。问题是Rc智能指针既不是Send也不是Sync。这意味着我们的user类型也不再是Send或Sync。这很糟糕,因为我们库的用户不能再在线程之间传递这个类型。为了解决这个问题,我们可以使用原子引用计数智能指针Arc来代替引用计数智能指针。

Arc是Send和Sync的,所以我们的类型可以再次在线程之间传递。但正如你刚才看到的,很容易添加一个不是Send或Sync的类型。那么我们如何防止这种情况呢?Jon Gjengset在他的书《Rust for Rustaceans》中给了我们一个聪明的技巧,我强烈推荐你阅读这本书。

首先,我们会实现一个名为is_normal的函数。

然后我们会创建一个简单的测试函数。is_normal定义了一个必须实现Send、Sync、Sized和Unpin特性的泛型类型T。这些都是正常类型应该实现的自动特性。注意,T在这个函数中没有被使用,这完全没问题。我们在测试中要做的就是调用那个函数,用我们想要测试的类型替换T。这将确保user实现了这四个特性。

让我们看看如果我们回到使用Rc智能指针会发生什么。

如你所见,我们现在得到了一个编译时错误,表示Rc不能在线程间安全发送。我们甚至不需要运行这个测试,因为这些特性约束在编译时就被检查了。

好的,让我们切换回使用Arc智能指针。

最后,让我们谈谈你的类型绝对应该实现的一组不在标准库中的特性。这些特性是来自serde crate的Serialize和Deserialize。这将允许你的类型在常见格式中进行序列化和反序列化,例如JSON。要实现这些特性,我们必须首先添加serde crate作为依赖。

我们还会添加serde_json作为依赖。

现在我们可以从serde派生Serialize和Deserialize。首先我们必须导入这些crate。

然后我们会将它们添加到Role和User。

这里我们得到一个错误,因为这个类型没有实现Deserialize特性。我们可以修复这个问题,但在这个例子中,我们不需要序列化这个字段,所以我们会通过添加一个属性来跳过它。

很好,我们的类型现在可以被序列化和反序列化了。让我们看看在main函数中它是如何工作的。

首先我们会创建一个JSON字符串。

然后我们会将字符串反序列化为一个user类型。

最后用debug格式打印user。

太好了,一切都如预期工作。

这里要提到的最后一件事是,你可能不想默认实现Serialize和Deserialize,即使这非常常见和有用。有些用户不需要这个功能。通常,库会在特性标志后面隐藏这个功能。这样,用户只有在需要这个功能时才会依赖serde。让我们看看这是如何工作的。首先,我们要打开Cargo.toml并将serde设为可选依赖。

然后我们会添加一个名为”serde”的新特性。

当这个特性启用时,serde依赖就会被包含进来。

最后在main中,我们会把我们的代码放在新的特性标志后面。

首先,我们用一个cfg表达式来控制导入Deserialize和Serialize。

然后我们会用一个cfg属性表达式来控制Deserialize和Serialize的实现。

我们会对user类型做同样的事情。

最后,我们会控制main中serde_json的使用。

然后,如果我们运行代码时没有启用任何特性,我们只会得到一个println语句。

但如果我们启用serde特性,我们会得到第二个println语句。

实现了这些特性后,你的类型现在已经准备好与世界分享了。

这就是本视频的全部内容。在你离开之前,请确保通过访问letsgetrusty.com/cheatsheet获取你的免费Rust备忘单。如果你喜欢这个视频,请确保给它点赞,并订阅频道以获取每周的Rust内容。说完这些,我们下次再见。

文章目录