Rust 标记Trait,公共词汇Trait

2024-06-07 14:38:15 浏览数 (2)

科学无非就是在自然界的多样性中寻求统一性(或者更确切地说,是在我们经验的多样性中寻求统一性)。用 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 不能将无固定大小的值存储在变量中或将它们作为参数传递。你只能通过像 &strBox<dyn Write> 这样的本身是固定大小的指针来处理它们。如上图所示,指向无固定大小值的指针始终是一个胖指针,宽度为两个机器字:指向切片的指针带有切片的长度,Trait对象带有指向方法实现的虚表的指针

尽管存在一些限制,但无固定大小类型能让 Rust 的类型系统工作得更顺畅

Clone

std::clone::Clone Trait适用于可复制自身的类型。Clone 定义如下:

代码语言:javascript复制
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 类型,其定义如下所示:

代码语言:javascript复制
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:

代码语言:javascript复制
trait Default {
    fn default() -> Self;
}

default 方法只会返回一个 Self 类型的新值。为 String 实现 Default 的代码一目了然:

代码语言:javascript复制
impl Default for String {
    fn default() -> String {
        String::new()
    }
}

Rust 的所有集合类型(VecHashMapBinaryHeap 等)都实现了 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>,那么就意味着你可以高效地从中借入 &TAsMutAsRef 针对可变引用的对应类型。它们的定义如下所示:

代码语言:javascript复制
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 函数的声明如下:

代码语言:javascript复制
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 的定义基本相同,只是名称变了:

代码语言:javascript复制
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

代码语言:javascript复制
trait BorrowMut<Borrowed: ?Sized>: Borrow<Borrowed> {
    fn borrow_mut(&mut self) -> &mut Borrowed;
}

刚才讲过的对 Borrow 的要求同样适用于 BorrowMut

ToOwned

给定一个引用,如果此类型实现了 std::clone::Clone,则生成其引用目标的拥有型副本的常用方法是调用 clone。但是当你想克隆一个 &str&[i32] 时该怎么办呢?你想要的可能是 StringVec<i32>,但 Clone 的定义不允许这样做:根据定义,克隆 &T 必须始终返回 T 类型的值,并且 str[u8] 是无固定大小类型,它们甚至都不是函数所能返回的类型。

std::borrow::ToOwned Trait提供了一种稍微宽松的方式来将引用转换为拥有型的值:

代码语言:javascript复制
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 的定义如下所示:

代码语言:javascript复制
enum Cow<'a, B: ?Sized>
    where B: ToOwned
{
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

Cow<B> 要么借入对 B 的共享引用,要么拥有可供借入此类引用的值。由于 Cow 实现了 Deref,因此你可以像对 B 的共享引用一样调用它的方法:如果它是 Owned,就会借入对拥有值的共享引用;如果它是 Borrowed,就会转让自己持有的引用。

还可以通过调用 Cowto_mut 方法来获取对 Cow 值的可变引用,这个方法会返回 &mut B。如果 Cow 恰好是 Cow::Borrowed,那么 to_mut 只需调用引用的 to_owned 方法来获取其引用目标的副本,将 Cow 更改为 Cow::Owned,并借入对新创建的这个拥有型值的可变引用即可。这就是此类型名称所指的“写入时克隆”行为。

类似地,Cow 还有一个 into_owned 方法,该方法会在必要时提升对所拥有值的引用并返回此引用,这会将所有权转移给调用者并在此过程中消耗掉 Cow

Cow 的一个常见用途是返回静态分配的字符串常量或由计算得来的字符串。假设你需要将错误枚举转换为错误消息。大多数变体可以用固定字符串来处理,但有些也需要在消息中包含附加数据

小结

Rust实用工具trait就都了解了,以目前的代码练习以及结合其他资料,这些新的概念Trait应该如何更好的应用,还需多敲代码,在实践中夯实基础

0 人点赞