科学无非就是在自然界的多样性中寻求统一性(或者更确切地说,是在我们经验的多样性中寻求统一性)。用 Coleridge 的话说,诗歌、绘画、艺术,同样是在多样性中寻求统一性。
——Jacob Bronowski
标记Trait
这类Trait多用作泛型类型变量的限界,以表达无法以其他方式捕获的约束条件。Sized 和 Copy 就属于这类Trait
公共词汇Trait
这类Trait不涉及任何编译器魔术,你完全可以在自己的代码中定义其等效Trait。之所以定义它们,是为了给常见问题制定一些约定俗成的解决方案。这对 crate 和模块之间的公共接口来说特别有价值:通过减少不必要的变体,让接口更容易理解,也增加了把来自不同 crate 的特性轻易插接在一起的可能性,而且无须样板代码或自定义胶水代码。
这类Trait包括 Default、引用借用Trait AsRef、AsMut、Borrow 与 BorrowMut、容错的转换Trait TryFrom 与 TryInto,以及 ToOwned Trait(对 Clone 的泛化)
实用工具Trait汇总表
代码语言:javascript复制析构器。每当丢弃一个值时,Rust 都要自动运行的清理代码
Sized
固定大小类型是指其每个值在内存中都有相同大小的类型。Rust 中的几乎所有类型都是固定大小的,比如每个 u64
占用 8 字节,每个 (f32, f32, f32)
元组占用 12 字节。甚至枚举也是有大小的,也就是说,无论实际存在的是哪个变体,枚举总会占据足够的空间来容纳其最大的变体。尽管 Vec<T>
拥有一个大小可变的堆分配缓冲区,但 Vec
值本身是指向“缓冲区、容量和长度”的指针,因此 Vec<T>
也是一个固定大小类型
所有固定大小类型都实现了 std::marker::Sized
Trait,该Trait没有方法或关联类型。Rust 自动为所有适用的类型实现了 std::marker::Sized
Trait,你不能自己实现它。Sized
的唯一用途是作为类型变量的限界:像 T: Sized
这样的限界要求 T
必须是在编译期已知的类型。由于 Rust 语言本身会使用这种类型的Trait为具有某些特征的类型打上标记,因此我们将其称为标记Trait
然而,Rust 也有一些无固定大小类型,它们的值大小不尽相同。例如,字符串切片类型 str
(注意没有 &
)就是无固定大小的。字符串字面量 "diminutive"
和 "big"
是对占用了 10 字节和 3 字节的 str
切片的引用,两者都展示在图 13-1 中。像 [T]
(同样没有 &
)这样的数组切片类型也是无固定大小的,即像 &[u8]
这样的共享引用可以指向任意大小的 [u8]
切片。因为 str
类型和 [T]
类型都表示不定大小的值集,所以它们是无固定大小类型
Rust 不能将无固定大小的值存储在变量中或将它们作为参数传递。你只能通过像 &str
或 Box<dyn Write>
这样的本身是固定大小的指针来处理它们。如上图所示,指向无固定大小值的指针始终是一个胖指针,宽度为两个机器字:指向切片的指针带有切片的长度,Trait对象带有指向方法实现的虚表的指针
尽管存在一些限制,但无固定大小类型能让 Rust 的类型系统工作得更顺畅
Clone
std::clone::Clone
Trait适用于可复制自身的类型。Clone
定义如下:
trait Clone: Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) {
*self = source.clone()
}
}
clone
方法应该为 self
构造一个独立的副本并返回它。由于此方法的返回类型是 Self
,并且函数本来也不可能返回无固定大小的值,因此 Clone
Trait也是扩展自 Sized
Trait的,进而导致其实现代码中的 Self
类型被限界成了 Sized
克隆一个值通常还需要为它拥有的任何值分配副本,因此 clone
无论在时间消耗还是内存占用方面都是相当昂贵的。例如,克隆 Vec<String>
不仅会复制此向量,还会复制它的每个 String
元素。这就是 Rust 不会自动克隆值,而是要求你进行显式方法调用的原因。像 Rc<T>
和 Arc<T>
这样的引用计数指针类型属于例外,即克隆其中任何一个都只会增加引用计数并为你返回一个新指针
Copy
对于大多数类型,赋值时会移动值,而不是复制它们。移动值可以更简单地跟踪它们所拥有的资源
例外情况:不拥有任何资源的简单类型可以是 Copy
类型,对这些简单类型赋值会创建源的副本,而不会移动值并使源回到未初始化状态
如果一个类型实现了 std::marker::Copy
标记Trait,那么它就是 Copy
类型,其定义如下所示:
trait Copy: Clone { }
对于你自己的类型,这当然很容易实现:
代码语言:javascript复制impl Copy for MyType { }
但由于 Copy
是一种对语言有着特殊意义的标记Trait,因此只有当类型需要一个浅层的逐字节复制时,Rust 才允许它实现 Copy
。拥有任何其他资源(比如堆缓冲区或操作系统句柄)的类型都无法实现 Copy
任何实现了 Drop
Trait的类型都不能是 Copy
类型。Rust 认为如果一个类型需要特殊的清理代码,那么就必然需要特殊的复制代码,因此不能是 Copy
类型
与 Clone
一样,可以使用 #[derive(Copy)]
让 Rust 为你派生出 Copy
实现
在允许一个类型成为 Copy
类型之前务必慎重考虑。尽管这样做能让该类型更易于使用,但也对其实现施加了严格的限制。如果复制的开销很高,那么就不适合进行隐式复制
Default
某些类型具有合理的默认值:向量或字符串默认为空、数值默认为 0、Option
默认为 None
,等等。这样的类型都可以实现 std::default::Default
Trait:
trait Default {
fn default() -> Self;
}
default
方法只会返回一个 Self
类型的新值。为 String
实现 Default
的代码一目了然:
impl Default for String {
fn default() -> String {
String::new()
}
}
Rust 的所有集合类型(Vec
、HashMap
、BinaryHeap
等)都实现了 Default
,其 default
方法会返回一个空集合
Default
的另一个常见用途是为表示大量参数集合的结构体生成默认值,其中大部分参数通常不用更改
如果类型 T
实现了 Default
,那么标准库就会自动为 Rc<T>
、Arc<T>
、Box<T>
、Cell<T>
、RefCell<T>
、Cow<T>
、Mutex<T>
和 RwLock<T>
实现 Default
如果一个元组类型的所有元素类型都实现了 Default
,那么该元组类型也同样会实现 Default
,这个元组的默认值包含每个元素的默认值。
Rust 不会为结构体类型隐式实现 Default
,但是如果结构体的所有字段都实现了 Default
,则可以使用 #[derive(Default)]
为此结构体自动实现 Default
AsRef 与 AsMut
如果一个类型实现了 AsRef<T>
,那么就意味着你可以高效地从中借入 &T
。AsMut
是 AsRef
针对可变引用的对应类型。它们的定义如下所示:
trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
trait AsMut<T: ?Sized> {
fn as_mut(&mut self) -> &mut T;
}
例如,Vec<T>
实现了 AsRef<[T]>
,而 String
实现了 AsRef<str>
。还可以把 String
的内容借入为字节数组,因此 String
也实现了 AsRef<[u8]>
。
AsRef
通常用于让函数更灵活地接受其参数类型。例如,std::fs::File::open
函数的声明如下:
fn open<P: AsRef<Path>>(path: P) -> Result<File>
open
真正想要的是 &Path
,即代表文件系统路径的类型。有了这个函数签名,open
就能接受可以从中借入 &Path
的一切,也就是实现了 AsRef<Path>
的一切
Borrow 与 BorrowMut
std::borrow::Borrow
Trait类似于 AsRef
:如果一个类型实现了 Borrow<T>
,那么它的 borrow
方法就能高效地从自身借入一个 &T
。但是 Borrow
施加了更多限制:只有当 &T
能通过与它借来的值相同的方式进行哈希和比较时,此类型才应实现 Borrow<T>
。(Rust 并不强制执行此限制,它只是记述了此Trait的意图。)这使得 Borrow
在处理哈希表和树中的键或者处理因为某些原因要进行哈希或比较的值时非常有用
这在区分对 String
的借用时很重要,比如 String
实现了 AsRef<str>
、AsRef<[u8]>
和 AsRef<Path>
,但这 3 种目标类型通常具有不一样的哈希值。只有 &str
切片才能保证像其等效的 String
一样进行哈希,因此 String
只实现了 Borrow<str>
。
Borrow
的定义与 AsRef
的定义基本相同,只是名称变了:
trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}
Vec<T>
和 [T; N]
实现了 Borrow<[T]>
。每个类似字符串的类型都能借入其相应的切片类型:String
实现了 Borrow<str>
、PathBuf
实现了 Borrow<Path>
,等等。标准库中所有关联集合类型都使用 Borrow
来决定哪些类型可以传给它们的查找函数。
标准库中包含一个通用实现,因此每个类型 T
都可以从自身借用:T: Borrow<T>
。这确保了在 HashMap<K, V>
中查找条目时 &K
总是可接受的类型。
为便于使用,每个 &mut T
类型也都实现了 Borrow<T>
,它会像往常一样返回一个共享引用 &T
。这样你就可以给集合的查找函数传入可变引用,而不必重新借入共享引用,以模拟 Rust 通常会从可变引用到共享引用进行的隐式转换。
BorrowMut
Trait则类似于针对可变引用的 Borrow
:
trait BorrowMut<Borrowed: ?Sized>: Borrow<Borrowed> {
fn borrow_mut(&mut self) -> &mut Borrowed;
}
刚才讲过的对 Borrow
的要求同样适用于 BorrowMut
ToOwned
给定一个引用,如果此类型实现了 std::clone::Clone
,则生成其引用目标的拥有型副本的常用方法是调用 clone
。但是当你想克隆一个 &str
或 &[i32]
时该怎么办呢?你想要的可能是 String
或 Vec<i32>
,但 Clone
的定义不允许这样做:根据定义,克隆 &T
必须始终返回 T
类型的值,并且 str
和 [u8]
是无固定大小类型,它们甚至都不是函数所能返回的类型。
std::borrow::ToOwned
Trait提供了一种稍微宽松的方式来将引用转换为拥有型的值:
trait ToOwned {
type Owned: Borrow<Self>;
fn to_owned(&self) -> Self::Owned;
}
与必须精确返回 Self
类型的 clone
不同,to_owned
可以返回任何能让你从中借入 &Self
的类型:Owned
类型必须实现 Borrow<Self>
。你可以从 Vec<T>
借入 &[T]
,所以只要 T
实现了 Clone
,[T]
就能实现 ToOwned<Owned=Vec<T>>
,这样就可以将切片的元素复制到向量中了。同样,str
实现了 ToOwned<Owned=String>
,Path
实现了 ToOwned<Owned=PathBuf>
,等等
Borrow 与 ToOwned 的实际运用:谦卑的 Cow
谦卑:指不会主动占有资源,直到确有必要
要想用好 Rust,就必然涉及对所有权问题的透彻思考,比如函数应该通过引用还是值接受参数。通常你可以任选一种方式,让参数的类型反映你的决定。但在某些情况下,在程序开始运行之前你无法决定是该借用还是该拥有,std::borrow::Cow
类型(用于“写入时克隆”,clone on write 的缩写)提供了一种兼顾两者的方式。
std::borrow::Cow
的定义如下所示:
enum Cow<'a, B: ?Sized>
where B: ToOwned
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
Cow<B>
要么借入对 B
的共享引用,要么拥有可供借入此类引用的值。由于 Cow
实现了 Deref
,因此你可以像对 B
的共享引用一样调用它的方法:如果它是 Owned
,就会借入对拥有值的共享引用;如果它是 Borrowed
,就会转让自己持有的引用。
还可以通过调用 Cow
的 to_mut
方法来获取对 Cow
值的可变引用,这个方法会返回 &mut B
。如果 Cow
恰好是 Cow::Borrowed
,那么 to_mut
只需调用引用的 to_owned
方法来获取其引用目标的副本,将 Cow
更改为 Cow::Owned
,并借入对新创建的这个拥有型值的可变引用即可。这就是此类型名称所指的“写入时克隆”行为。
类似地,Cow
还有一个 into_owned
方法,该方法会在必要时提升对所拥有值的引用并返回此引用,这会将所有权转移给调用者并在此过程中消耗掉 Cow
。
Cow
的一个常见用途是返回静态分配的字符串常量或由计算得来的字符串。假设你需要将错误枚举转换为错误消息。大多数变体可以用固定字符串来处理,但有些也需要在消息中包含附加数据
小结
Rust实用工具trait就都了解了,以目前的代码练习以及结合其他资料,这些新的概念Trait应该如何更好的应用,还需多敲代码,在实践中夯实基础