附录 D:派生特征 trait

在本书的各个部分中,我们讨论了可应用于结构体和枚举定义的 derive 属性。被 derive 标记的对象会自动实现对应的默认特征代码,继承相应的功能。

在本附录中,我们列举了所有标准库存在的 derive 特征,每个特征覆盖了以下内容

  • 该特征将会派生什么样的操作符和方法
  • derive 提供什么样的特征实现
  • 实现特征对于类型意味着什么
  • 你需要什么条件来实现该特征
  • 特征示例

如果你希望不同于 derive 属性所提供的行为,请查阅 标准库文档 中每个特征的细节以了解如何手动实现它们。

除了本文列出的特征之外,标准库中定义的其它特征不能通过 derive 在类型上实现。这些特征不存在有意义的默认行为,所以由你负责以合理的方式实现它们。

一个无法被派生的特征例子是为终端用户处理格式化的 Display 。你应该时常考虑使用合适的方法来为终端用户显示一个类型。终端用户应该看到类型的什么部分?他们会找出相关部分吗?对他们来说最关心的数据格式是什么样的?Rust 编译器没有这样的洞察力,因此无法为你提供合适的默认行为。

本附录所提供的可派生特征列表其实并不全面:库可以为其内部的特征实现 derive ,因此除了本文列出的标准库 derive 之外,还有很多很多其它库的 derive 。实现 derive 涉及到过程宏的应用,这在宏章节中有介绍。

用于开发者输出的 Debug

Debug 特征可以让指定对象输出调试格式的字符串,通过在 {} 占位符中增加 :? 表明,例如println!("show you some debug info: {:?}", MyObject);.

Debug 特征允许以调试为目的来打印一个类型的实例,所以程序员可以在执行过程中看到该实例的具体信息。

例如,在使用 assert_eq! 宏时, Debug 特征是必须的。如果断言失败,这个宏就把给定实例的值打印出来,这样程序员就能看到两个实例为什么不相等。

等值比较的 PartialEqEq

PartialEq 特征可以比较一个类型的实例以检查是否相等,并开启了 ==!= 运算符的功能。

派生的 PartialEq 实现了 eq 方法。当 PartialEq 在结构体上派生时,只有所有 的字段都相等时两个实例才相等,同时只要有任何字段不相等则两个实例就不相等。当在枚举上派生时,每一个成员都和其自身相等,且和其他成员都不相等。

例如,当使用 assert_eq! 宏时,需要比较一个类型的两个实例是否相等,则 PartialEq 特征是必须的。

Eq 特征没有方法, 其作用是表明每一个被标记类型的值都等于其自身。 Eq 特征只能应用于那些实现了 PartialEq 的类型,但并非所有实现了 PartialEq 的类型都可以实现 Eq。浮点类型就是一个例子:浮点数的实现表明两个非数字( NaN ,not-a-number)值是互不相等的。

例如,对于一个 HashMap<K, V> 中的 key 来说, Eq 是必须的,这样 HashMap<K, V> 就可以知道两个 key 是否一样。

次序比较的 PartialOrdOrd

PartialOrd 特征可以让一个类型的多个实例实现排序功能。实现了 PartialOrd 的类型可以使用 <><=>= 操作符。一个类型想要实现 PartialOrd 的前提是该类型已经实现了 PartialEq

派生 PartialOrd 实现了 partial_cmp 方法,一般情况下其返回一个 Option<Ordering>,但是当给定的值无法进行排序时将返回 None。尽管大多数类型的值都可以比较,但一个无法产生顺序的例子是:浮点类型的非数字值。当在浮点数上调用 partial_cmp 时, NaN 的浮点数将返回 None

当在结构体上派生时, PartialOrd 以在结构体定义中字段出现的顺序比较每个字段的值来比较两个实例。当在枚举上派生时,认为在枚举定义中声明较早的枚举项小于其后的枚举项。

例如,对于来自于 rand 包的 gen_range 方法来说,当在一个大值和小值指定的范围内生成一个随机值时, PartialOrd trait 是必须的。

对于派生了 Ord 特征的类型,任何两个该类型的值都能进行排序。 Ord 特征实现了 cmp 方法,它返回一个 Ordering 而不是 Option<Ordering>,因为总存在一个合法的顺序。一个类型要想使用 Ord 特征,它必须要先实现 PartialOrdEq 。当在结构体或枚举上派生时, cmp 方法 和 PartialOrdpartial_cmp 方法表现是一致的。

例如,当在 BTreeSet<T>(一种基于有序值存储数据的数据结构)上存值时, Ord 是必须的。

复制值的 CloneCopy

Clone 特征用于创建一个值的深拷贝(deep copy),复制过程可能包含代码的执行以及堆上数据的复制。查阅 通过 Clone 进行深拷贝获取有关 Clone 的更多信息。

派生 Clone 实现了 clone 方法,当为整个的类型实现 Clone 时,在该类型的每一部分上都会调用 clone 方法。这意味着类型中所有字段或值也必须实现了 Clone,这样才能够派生 Clone

例如,当在一个切片(slice)上调用 to_vec 方法时, Clone 是必须的。切片只是一个引用,并不拥有其所包含的实例数据,但是从 to_vec 中返回的 Vector 需要拥有实例数据,因此, to_vec 需要在每个元素上调用 clone 来逐个复制。因此,存储在切片中的类型必须实现 Clone

Copy 特征允许你通过只拷贝存储在栈上的数据来复制值(浅拷贝),而无需复制存储在堆上的底层数据。查阅 通过 Copy 复制栈数据 的部分来获取有关 Copy 的更多信息。

实际上 Copy 特征并不阻止你在实现时使用了深拷贝,只是,我们不应该这么做,毕竟遵循一个语言的惯例是很重要的。当用户看到 Copy 时,潜意识就应该知道这是浅拷贝,复制一个值会非常快。

当一个类型的内部字段全部实现了 Copy 时,你就可以在该类型上派上 Copy 特征。 一个类型如果要实现 Copy 它必须先实现 Clone ,因为一个类型实现 Clone 后,就等于顺便实现了 Copy

总之, Copy 拥有更好的性能,当浅拷贝足够的时候,就不要使用 Clone ,不然会导致你的代码运行更慢,对于性能优化来说,一个很大的方面就是减少热点路径深拷贝的发生。

固定大小的值映射的 Hash

Hash 特征允许你使用 hash 函数把一个任意大小的实例映射到一个固定大小的值上。派生 Hash 实现了 hash 方法,对某个类型进行 hash 调用,其实就是对该类型下每个字段单独进行 hash 调用,然后把结果进行汇总,这意味着该类型下的所有的字段也必须实现了 Hash,这样才能够派生 Hash

例如,在 HashMap<K, V> 上存储数据,存放 key 的时候, Hash 是必须的。

默认值的 Default

Default 特征会帮你创建一个类型的默认值。 派生 Default 意味着自动实现了 default 函数。 default 函数的派生实现调用了类型每部分的 default 函数,这意味着类型中所有的字段也必须实现了 Default,这样才能够派生 Default

Default::default 函数通常结合结构体更新语法一起使用,这在第五章的 结构体更新语法 部分有讨论。可以自定义一个结构体的一小部分字段而剩余字段则使用 ..Default::default() 设置为默认值。

例如,当你在 Option<T> 实例上使用 unwrap_or_default 方法时, Default 特征是必须的。如果 Option<T>None 的话, unwrap_or_default 方法将返回 T 类型的 Default::default 的结果。