|
|
|
|
## 附录 D:派生特征 trait
|
|
|
|
|
|
|
|
|
|
在本书的各个部分中,我们讨论了可应用于结构体和枚举定义的 `derive` 属性。被 `derive` 标记的对象会自动实现对应的默认特征代码,继承相应的功能。
|
|
|
|
|
|
|
|
|
|
在本附录中,我们列举了所有标准库存在的 `derive` 特征,每个特征覆盖了以下内容
|
|
|
|
|
|
|
|
|
|
- 该特征将会派生什么样的操作符和方法
|
|
|
|
|
- 由 `derive` 提供什么样的特征实现
|
|
|
|
|
- 实现特征对于类型意味着什么
|
|
|
|
|
- 你需要什么条件来实现该特征
|
|
|
|
|
- 特征示例
|
|
|
|
|
|
|
|
|
|
如果你希望不同于 `derive` 属性所提供的行为,请查阅 [标准库文档](https://doc.rust-lang.org/std/index.html) 中每个特征的细节以了解如何手动实现它们。
|
|
|
|
|
|
|
|
|
|
除了本文列出的特征之外,标准库中定义的其它特征不能通过 `derive` 在类型上实现。这些特征不存在有意义的默认行为,所以由你负责以合理的方式实现它们。
|
|
|
|
|
|
|
|
|
|
一个无法被派生的特征例子是为终端用户处理格式化的 `Display` 。你应该时常考虑使用合适的方法来为终端用户显示一个类型。终端用户应该看到类型的什么部分?他们会找出相关部分吗?对他们来说最关心的数据格式是什么样的?Rust 编译器没有这样的洞察力,因此无法为你提供合适的默认行为。
|
|
|
|
|
|
|
|
|
|
本附录所提供的可派生特征列表其实并不全面:库可以为其内部的特征实现 `derive` ,因此除了本文列出的标准库 `derive` 之外,还有很多很多其它库的 `derive` 。实现 `derive` 涉及到过程宏的应用,这在[宏章节](https://course.rs/advance/macro.html)中有介绍。
|
|
|
|
|
|
|
|
|
|
### 用于开发者输出的 `Debug`
|
|
|
|
|
|
|
|
|
|
`Debug` 特征可以让指定对象输出调试格式的字符串,通过在 `{}` 占位符中增加 `:?` 表明,例如`println!("show you some debug info: {:?}", MyObject);`.
|
|
|
|
|
|
|
|
|
|
`Debug` 特征允许以调试为目的来打印一个类型的实例,所以程序员可以在执行过程中看到该实例的具体信息。
|
|
|
|
|
|
|
|
|
|
例如,在使用 `assert_eq!` 宏时, `Debug` 特征是必须的。如果断言失败,这个宏就把给定实例的值打印出来,这样程序员就能看到两个实例为什么不相等。
|
|
|
|
|
|
|
|
|
|
### 等值比较的 `PartialEq` 和 `Eq`
|
|
|
|
|
|
|
|
|
|
`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 是否一样。
|
|
|
|
|
|
|
|
|
|
### 次序比较的 `PartialOrd` 和 `Ord`
|
|
|
|
|
|
|
|
|
|
`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` 特征,它必须要先实现 `PartialOrd` 和 `Eq` 。当在结构体或枚举上派生时, `cmp` 方法 和 `PartialOrd` 的 `partial_cmp` 方法表现是一致的。
|
|
|
|
|
|
|
|
|
|
例如,当在 `BTreeSet<T>`(一种基于有序值存储数据的数据结构)上存值时, `Ord` 是必须的。
|
|
|
|
|
|
|
|
|
|
### 复制值的 `Clone` 和 `Copy`
|
|
|
|
|
|
|
|
|
|
`Clone` 特征用于创建一个值的深拷贝(deep copy),复制过程可能包含代码的执行以及堆上数据的复制。查阅 [通过 Clone 进行深拷贝](https://course.rs/basic/ownership/ownership.html#克隆深拷贝)获取有关 `Clone` 的更多信息。
|
|
|
|
|
|
|
|
|
|
派生 `Clone` 实现了 `clone` 方法,当为整个的类型实现 `Clone` 时,在该类型的每一部分上都会调用 `clone` 方法。这意味着类型中所有字段或值也必须实现了 `Clone`,这样才能够派生 `Clone` 。
|
|
|
|
|
|
|
|
|
|
例如,当在一个切片(slice)上调用 `to_vec` 方法时, `Clone` 是必须的。切片只是一个引用,并不拥有其所包含的实例数据,但是从 `to_vec` 中返回的 Vector 需要拥有实例数据,因此, `to_vec` 需要在每个元素上调用 `clone` 来逐个复制。因此,存储在切片中的类型必须实现 `Clone`。
|
|
|
|
|
|
|
|
|
|
`Copy` 特征允许你通过只拷贝存储在栈上的数据来复制值(浅拷贝),而无需复制存储在堆上的底层数据。查阅 [通过 Copy 复制栈数据](https://course.rs/basic/ownership/ownership.html#拷贝浅拷贝) 的部分来获取有关 `Copy` 的更多信息。
|
|
|
|
|
|
|
|
|
|
实际上 `Copy` 特征并不阻止你在实现时使用了深拷贝,只是,我们不应该这么做,毕竟遵循一个语言的惯例是很重要的。当用户看到 `Copy` 时,潜意识就应该知道这是浅拷贝,复制一个值会非常快。
|
|
|
|
|
|
|
|
|
|
当一个类型的内部字段全部实现了 `Copy` 时,你就可以在该类型上派上 `Copy` 特征。 一个类型如果要实现 `Copy` 它必须先实现 `Clone` ,因为一个类型实现 `Clone` 后,就等于顺便实现了 `Copy` 。
|
|
|
|
|
|
|
|
|
|
总之, `Copy` 拥有更好的性能,当浅拷贝足够的时候,就不要使用 `Clone` ,不然会导致你的代码运行更慢,对于[性能优化](https://course.rs/profiling/performance/intro.html)来说,一个很大的方面就是减少热点路径深拷贝的发生。
|
|
|
|
|
|
|
|
|
|
### 固定大小的值映射的 `Hash`
|
|
|
|
|
|
|
|
|
|
`Hash` 特征允许你使用 `hash` 函数把一个任意大小的实例映射到一个固定大小的值上。派生 `Hash` 实现了 `hash` 方法,对某个类型进行 `hash` 调用,其实就是对该类型下每个字段单独进行 `hash` 调用,然后把结果进行汇总,这意味着该类型下的所有的字段也必须实现了 `Hash`,这样才能够派生 `Hash`。
|
|
|
|
|
|
|
|
|
|
例如,在 `HashMap<K, V>` 上存储数据,存放 key 的时候, `Hash` 是必须的。
|
|
|
|
|
|
|
|
|
|
### 默认值的 `Default`
|
|
|
|
|
|
|
|
|
|
`Default` 特征会帮你创建一个类型的默认值。 派生 `Default` 意味着自动实现了 `default` 函数。 `default` 函数的派生实现调用了类型每部分的 `default` 函数,这意味着类型中所有的字段也必须实现了 `Default`,这样才能够派生 `Default` 。
|
|
|
|
|
|
|
|
|
|
`Default::default` 函数通常结合结构体更新语法一起使用,这在第五章的 [结构体更新语法](https://course.rs/basic/compound-type/struct.html#结构体更新语法) 部分有讨论。可以自定义一个结构体的一小部分字段而剩余字段则使用 `..Default::default()` 设置为默认值。
|
|
|
|
|
|
|
|
|
|
例如,当你在 `Option<T>` 实例上使用 `unwrap_or_default` 方法时, `Default` 特征是必须的。如果 `Option<T>` 是 `None` 的话, `unwrap_or_default` 方法将返回 `T` 类型的 `Default::default` 的结果。
|