:::info 本章内容改编自《Programming Rust, 2nd Edition》的第13章。 :::

本章描述了我们所谓的Rust的utility traitsutility traits是Rust事先为你准备好的trait,省去了你自己定义的过程,能够直接使用。它们可分为三大类:

  • 语言扩展特征(language extension traits)

如果你知道Rust 的操作符重载的traits,那就很简单了,因为language extension traits与操作符重载traits很类似。之所以称为语言拓展特征,是因为这些特征允许你将你自己定义的类型与Rust语言更紧密地集成在一起。tarits包括:Drop,DerefDerefMut,以类型转换traitsFromInto。我们将在稍后详细介绍。

  • 标志特征(Marker traits)

这些特征主要用于绑定泛型变量,以表达你想施加,但并无其他有效手段的约束。这些包括SizedCopy(我们在之前的Struct上已经使用过)。

  • 公共词汇特征(Public vocabulary traits)

这一类traits没有任何特殊的编译器支持,你完全可以在自己的代码中定义等价的特征。但他们的重要目标是为常见问题制定更加普适的解决方案。这些在cratesmodule之间的公共接口中特别有价值:通过减少不必要的变化,它们使接口更容易理解,当然使用这些trait也有弊端,就是增加了不同crates被强行组装在一起的风险,强行组装指在这个过程中并没有定制化的代码处理它们之间的组装关系。public vocabulary traits包括Default,引用借用特性AsRef, AsMut,BorrowBorrowMut,容易出错的转换特征 TryFromTryInto;以及ToOwned特征,即Clone的泛型。

总结:

截屏2022-06-21 23.26.18.png

Drop

当一个value的所有者注销时,我们说Rust drop了这个valuedrop一个value需要释放该value所拥有的任何其他值、堆存储和系统资源。drop通常会在各种情况下发生:

  • 当变量超出它的作用域时;
  • 在表达式语句的末尾;
  • 当你删除一个向量,从它的末端移除元素;

当然可能还会有一些情况,但是并不常见,我们只考虑这三种。在以上提到的情况下,Rust通常会自动处理删除值。

举个例子,如果定义了以下类型:

  1. struct Appellation {
  2. name: String,
  3. nicknames: Vec<String>
  4. }

解释一下这个struct:一个Appellation拥有字符串内容的堆存储和vector元素的缓冲区。

当一个Appellation被drop时,Rust会负责清理所有这些东西,而不需要你进行任何进一步的手动编码。然而,如果你想,你可以通过实现std::ops::Drop trait来定制Rust如何删除你的类型的值:

  1. trait Drop {
  2. fn drop(&mut self);
  3. }

Drop的实现类似于c++中的析构函数(destructor)或其他语言中的终结器(finalizer)。当一个值被删除时,如果它实现了std::ops::Drop, Rust调用它的Drop方法,然后继续删除它的字段或元素所拥有的值。这种对drop的隐式调用是调用Drop的唯一方法;如果你试图显式地调用它,Rust会报错。

因为Rust总是在删除一个值的字段或元素之前调用Drop:: Drop,在调用方法时,需要保证这个值仍然是有意义的。换句话说,当显式调用Drop之后,在值消亡的时候,Rust再自动调用Drop,会使得这个值被删除两次。

我们自定义一个Appellation类型的Drop实现:

  1. impl Drop for Appellation {
  2. fn drop(&mut self) {
  3. print!("Dropping {}", self.name);
  4. if !self.nicknames.is_empty() {
  5. print!(" (AKA {})", self.nicknames.join(", "));
  6. }
  7. println!("");
  8. }
  9. }

当应用这个Trait之后,我们的代码可以做出如下更改:

  1. {
  2. let mut a = Appellation {
  3. name: "Zeus".to_string(),
  4. nicknames: vec!["cloud collector".to_string(),
  5. "king of the gods".to_string()]
  6. };
  7. println!("before assignment");
  8. a = Appellation { name: "Hera".to_string(), nicknames: vec![] };
  9. println!("at end of block");
  10. }

当我们将第二个Appellation赋给a时,第一个会被丢弃,而当我们离开a的作用域时,第二个也会被丢弃。

这段代码打印了以下内容:

  1. before assignment
  2. Dropping Zeus (AKA cloud collector, king of the gods)
  3. at end of block
  4. Dropping Hera

我们对Appellation的std::ops::Drop自定义实现,除了打印消息外什么都没有做,那么它的内存到底是如何被清理的呢?

我们首先澄清一个事实:Vec类型实现了Drop,在Vec需要消亡的时候,Rust自动删除它的每个元素,然后释放它们占用的堆分配的缓冲区。

然后来看String类型:String在内部使用Vec来保存它的文本,所以String不需要在自身实现Drop;它让Vec负责释放角色。

同样的原则也适用于Appellation:当一个元素被删除时,Vec自身实现的Drop实际上负责释放每个字符串的内容,并最终释放存放vector元素的缓冲区。

至于存储Appellation值本身的内存,它也有一些所有者,可能是一个本地变量或一些数据结构,因此总有值负责释放它。

如果一个变量的值被移动到其他地方,那么当它超出作用域时,这个变量是未初始化的,那么Rust就不会尝试删除这个变量:因为它里面没有值可以删除。

在一些情况下,一个变量的值有可能被移走,也可能没有(这取决于控制流),这一原则也适用。

在这样的情况下,Rust会用一个不可见的标志来跟踪可变变量的状态,该标志指示变量的值是否需要删除,举个例子:

  1. let p;
  2. {
  3. let q = Appellation { name: "Cardamine hirsuta".to_string(),
  4. nicknames: vec!["shotweed".to_string(),
  5. "bittercress".to_string()]};
  6. if complicated_condition() {
  7. p = q;
  8. }
  9. }
  10. println!("Sproing! What was that?");

在这个例子中,根据complicated_condition返回truefalse, p或q其中一个将最终拥有Appellation,而另一个未初始化。p和q被声明的位置决定了它是在println!之前还是之后被Drop,我们可以看到q在println!之前超出了作用域,p在println!后面才结束生命周期。对于Rust来说,这个Appellation的值有可能被移走,也可能不会,于是会有一个不可见的标志来跟踪Appellation的状态,指示它是否需要被删除。

从上面例子我们可以看到,虽然一个值可以发生所有权的转移,但是Rust只删除它一次。

你通常不需要实现std::ops::Drop,除非你定义的类型拥有Rust不知道的资源。例如,在Unix系统上,Rust的标准库内部使用以下类型来表示操作系统文件描述符:

  1. struct FileDesc {
  2. fd: c_int,
  3. }

FileDescfd字段只是文件描述符的编号,当程序用完它时我们应该把文件关闭;C_int是i32的别名。标准库对FileDesc的Drop实现如下:

  1. impl Drop for FileDesc {
  2. fn drop(&mut self) {
  3. let _ = unsafe { libc::close(self.fd) };
  4. }
  5. }

解释一下上述代码,libc::close是C库的close函数的Rust名称。这里之所以使用unsafe是因为,Rust只能在unsafe code中使用C的函数。

这里实现的目标是:我么你在Struct中定义了一个文件,在程序结束时,我们应该把文件关闭,但是Rust并没有默认识别出文件类型,所以我们在Drop上显式定义关闭方法。

如果一个类型实现了Drop,它就不能实现Copy特征。如果类型是Copy,这意味着简单的字节对字节复制足以产生该值的独立副本。但是,对相同的数据多次调用相同的drop方法通常是错误的。

标准引用库(prelude)包括一个删除值的函数,但它的定义很简单:

  1. fn drop<T>(_x: T){}

换句话说,它按值接收它的参数,从调用者那里获得所有权——然后对它什么也不做。当_x超出范围时,Rust会删除它的值,就像对任何其他变量一样。

Sized

Sized type是指这个type的所有值在内存中具有固定的大小。Rust中几乎所有类型都有固定大小:

  • 每个u64占用8个字节,
  • 每个(f32, f32, f32)元组自然是12字节(因为每个f32占据4个字节)
  • 甚至enum也是有大小的:无论实际存在哪个变体,枚举总是占据足够的空间容纳最大的变体
  • 虽然Vec<T>拥有一个堆分配的缓冲区,其大小可以变化,但Vec值本身是一个指向缓冲区、其容量和长度的指针,所以Vec<T>是一个Sized类型。

所有Sized类型都实现std::marker:: Sized trait,Sized这个trait上并没有定义任何的方法或者关联类型。不仅如此,你也不能自己实现Sized,Rust会自动把Sized应用在应该实现的类型上。

Sized的唯一用途是作为类型变量的边界:像T: Sized这样的边界,要求T是一个在编译时已知大小的类型。这种类型的特征被称为maker traits,因为Rust语言只是使用这样的trait将一些类型进行标记。

然而,Rust也有一些未确定大小的类型,它们的值并不都是固定的大小。例如,字符串切片类型str(注意,没有&)是无确定大小的。字符串字面值"diminutive""big"是对str切片的引用,分别占用10和3个字节。如下图所示。

[T](同样,没有&)这样的数组切片片类型也是无大小的:像&[u8]这样的共享引用可以指向任意大小的[u8]片。

因为str[T]类型表示一个可以更改的大小的值的集合,所以它们是usized类型。
截屏2022-06-21 23.32.27.png
[Figure]: References to unsized values

Rust中另一种常见的无确定大小类型是dyn类型,它是trait object的引用。正如我们在之前对“trait objects”中解释的那样,trait objects是一个指向实现给定特征的某个值的指针。

例如,类型&dyn std::io::WriteBox<dyn std::io::Write>是指向一些值的指针,这些值上实现了Write 的trait。实际上涉及的对象可以是一个文件或网络套接字,也可以是自己实现了Write的某些类型。由于实现了Write的类型有很多,因此我们把trait objectdyn Writes视作是unsized的,毕竟这个trait object 的大小有太多种可能。

Rust不能在变量中存储unsized的值,也不能将它们作为参数传递。你只能通过像&strBox<dyn std::io::Write>这样的指针来处理它们,因为这些指针本身是有大小的。

如上图所示,指向unsized值的指针总是一两个字宽的胖指针(fat pointer):指向切片的指针也携带切片的长度,trait对象也携带方法实现虚函数表的指针。

Trait对象和指向切片的指针很好地对称。在这两种情况下,类型都缺乏使用这个类型的必要信息:

  • 在不知道长度的情况下索引[u8]
  • 调用Box上的方法,但是不知道Box是否应用了Write在它引用值上

在这两种情况下,胖指针填充类型中缺失的信息,携带一个长度或虚函数表指针。省略的静态信息被替换为动态信息。

由于unsized类型受到了很多限制,所以大多数泛型类型变量应该限制为Sized类型。事实上,这是非常必要的,在Rust中它是隐式默认值:如果你写struct<T> S{…}, Rust理解你的意思是struct<T:Sized> S{…}。如果你不想以这种方式约束T,你必须显式地配置,写入struct<T: ?Sized> S{…}?sized语法是针对这种情况的,它的意思是“不一定是sized”。例如,如果你写结构体S<T:?Sized> {b: Box<T>},那么Rust将允许你写S<str>S<dyn Write>,其中Box成为一个胖指针,以及S<i32>S<String>,其中Box是一个普通指针。

尽管有这些限制,unsized类型使Rust的类型系统工作得更流畅。阅读标准库文档时,偶尔会遇到类型变量的? sized边界;这几乎总是意味着只指向给定的类型,并允许相关代码处理以下三种情况:

  • 切片
  • 特征对象
  • 普通值。

当一个类型变量有? sized边界时,人们经常说它的大小有问题:它可能是sized,也可能不是。

除了切片和特征对象之外,还有一种unsized类型。一个结构类型的最后一个字段(但仅仅是它的最后一个字段)可能是Unsized的,并且这样的Struct本身是unsized。例如,一个Rc<T>引用计数的指针被内部实现为一个指向私有类型RcBox的指针,它与T一起存储引用计数。下面是RcBox的一个简化定义:

  1. struct RcBox<T: ?Sized> {
  2. ref_count: usize,
  3. value: T,
  4. }

value字段是Rc<T>计数引用的值;Rc<T>对指向该字段的指针解引用。ref_count字段保存引用计数。

真正的标准库中,RcBox只是内部的一个实现细节,并不能在标准库外的代码中使用。

但假设我们用的是前面的定义。你可以在Sized类型上使用这个RcBox,如RcBox<String>;结果是一个Sized类型。或者你可以将它用于unsized的类型,比如RcBox<dyn std::fmt::Display>(其中Display是可以被println!及类似的宏格式化的trait);注意,应用了trait object的RcBox<dyn Display>是一个unsized的结构类型。

你不能直接构建RcBox<dyn Display>值。你首先需要创建一个普通的、大小的RcBox,它的值类型实现Display,比如RcBox<String>。然后Rust允许你将引用&RcBox<String>转换为fat reference&RcBox<dyn Display>,请看以下代码:

  1. let boxed_lunch: RcBox<String> = RcBox {
  2. ref_count: 1,
  3. value: "lunch".to_string()
  4. };
  5. use std::fmt::Display;
  6. let boxed_displayable: &RcBox<dyn Display> = &boxed_lunch;

当将值传递给函数时,转换会隐式发生,所以你可以将&RcBox<String>传递给期望&RcBox<dyn Display>的函数:

  1. fn display(boxed: &RcBox<dyn Display>) {
  2. println!("For your enjoyment: {}", &boxed.value);
  3. }
  4. display(&boxed_lunch);

结果打印为:

  1. For your enjoyment: lunch

Clone

std::clone::Clone特征用于能够复制自己的类型。Clone的定义如下:

  1. trait Clone: Sized {
  2. fn clone(&self) -> Self;
  3. fn clone_from(&mut self, source: &Self) {
  4. *self = source.clone()
  5. }
  6. }

clone方法应该构造一个独立的self副本并返回它。由于该方法的返回类型是Self,而函数可能不会返回无确定大小的值,因此clonetrait从某种意义上扩展了sized trait:这具有将实现的Self类型绑定为sized的效果。

克隆一个值通常需要分配它所拥有的任何东西的副本,因此克隆可能在时间和内存方面开销都很大。例如,克隆Vec<String>不仅复制vector,还复制它的每个String元素。这就是为什么Rust并不会自动地克隆值,而是要求程序员进行显式的方法调用。像Rc<T>Arc<T>这样的引用计数的指针类型是例外:克隆这两者其中一个只是增加引用计数并返回一个新指针。

clone_from方法将self修改为source的副本。clone_from的默认定义只是克隆source,然后将其移动到*self中。这通常来说是有效的,但对于某些类型,有一种更快的方法来获得相同的效果。例如,假设st是字符串。语句s = t.clone();必须克隆t,drops的旧值,然后将克隆的值移动到s中;这里涉及一个堆分配和一个堆回收。但是,如果属于原始s的堆缓冲区有足够的容量容纳t的内容,则不需要分配或释放:复制t的文本到s的缓冲区,并调整长度。显然这是一种优化,所以在泛型代码中,应该尽可能使用clone_from,以便有可能利用到clone_from的优化方式。

如果你的Clone实现只是简单地将clone应用到你的类型的每个字段或元素,然后从这些克隆构建一个新值,并且clone_from的默认定义足够好,那么Rust将为你实现:简单地在你的类型定义上面添加#[derive(Clone)]

标准库中几乎所有可以复制的类型都实现了Clone。原子类型,如booli32。容器类型如StringVec<T>HashMap也可以。有些类型没有复制的意义,比如std::sync::Mutex;这些没有实现克隆。某些类型如std::fs::File可以复制,但是如果操作系统没有必要的资源,复制可能会失败;这些类型没有实现Clone,因为Clone必须是绝对可靠的。相反,std::fs::File 提供了一个try_clone方法,它返回一个std::io::Result,如果克隆失败,这个方法可以报错。

Copy

在第4章中,我们解释过,对于大多数类型,赋值是move的行为,会转移所有权,而不是简单地copy值。move值使得跟踪它们所拥有的资源变得更加简单。

我们曾指出例外:不拥有任何资源的简单类型可以是Copy类型,即赋值对源进行复制,而不是move所有权。

当时,我们对Copy到底是什么没有明确的定义,但是现在我们可以告诉你:如果一个类型实现std::marker::Copy的marker trait,它就是Copy类型,定义如下:

  1. trait Copy: Clone{}

这个属性也非常易于应用到你自己的类型上:

  1. impl Copy for Mytype{}

但是因为Copy对于Rust来说是一个具有特殊意义的标记特征,所以Rust允许一个类型实现Copy时,前提是它只需要简单的逐字节复制。在拥有任何其他资源(如堆缓冲区或操作系统句柄)的类型上,不能实现Copy

前面我们讲过,任何实现Drop特征的类型都不能是Copy类型。Rust假定,如果一个类型需要特殊的清理代码,它也必须需要特殊的复制代码,因此不能是Copy

Clone一样,您可以使用#[derive(Copy)]让Rust派生使用Copy。你经常会看到这两者同时派生,使用[#derive(Clone, Copy)]

在做typeCopy之前要仔细考虑。尽管这样做使类型更容易使用,但它对其实现施加了严格的限制。隐式拷贝开销也很大。

Deref and Derefmut

如果你想修改解引用符号,例如&.,可以通过在自定义的类型上实现std::ops::Derefstd::ops::DerefMut特征来操作。像Box<T>Rc<T>这样的指针类型实现了这些特征,因此它们可以像Rust的build-in指针类型那样工作。

例如,如果你有一个Box<Complex>值b,那么*b指的是b指向的Complex值,而b.re指的是它的实际component。如果上下文指定或借用一个可变的引用到指针内部的实际值, Rust使用DerefMut(“解引用可变”)特征;其他情况下,只读访问就足够了,它使用Deref

特征是这样定义的:

  1. trait Deref {
  2. type Target: ?Sized;
  3. fn deref(&self) -> &Self::Target;
  4. }
  5. trait DerefMut: Deref {
  6. fn deref_mut(&mut self) -> &mut Self::Target;
  7. }

derefderef_mut方法接受一个&Self引用,并返回一个&Self::Target引用。Target应该是Self包含、拥有或引用的东西:对于Box<Complex>Target类型是Complex。请注意,DerefMut扩展了Deref::如果你可以解除引用并修改它,当然你也应该能够借用对它的共享引用。由于这些方法返回的引用与&self的生命周期相同,因此在返回的引用的生命周期内,self仍然是被借用的。

DerefDerefMut特性也发挥了另一个作用。由于deref接受一个&Self引用并返回一个&Self::Target引用,Rust使用这个来自动将前一种类型的引用转换为后一种类型的引用。换句话说,如果插入deref调用可以防止类型不匹配,那么Rust会自动插入一个deref。同理,实现DerefMut可以对可变引用进行相应的转换。这被称为deref胁迫:一种类型被胁迫表现出另一种类型的行为

deref在下列情况下很方便:

  • 如果你有一些Rc<String>值r,并且想要对其调用String::find,你可以简单地写r.find('?'),而不是(*r).find('?'):方法调用隐式地借用r,并且&Rc 胁迫到&String,因为Rc<T>实现Deref<Target = T>
  • 可以在String值上使用split_at这样的方法,尽管split_atstr切片类型的方法,因为String实现了Deref<Target = str>。String不需要重新实现str的所有方法,因为你可以从&String胁迫转换为&str
  • 如果你有一个字节向量v,你想把它传递给一个参数为字节切片&[u8]的函数,你可以简单地把&v作为参数,因为Vec<T>实现Deref<Target = T>

如有必要,Rust将连续应用几个强制deref转换。例如,使用前面提到的强制方法,你可以将split_at直接应用到Rc<String>,因为&Rc<Stirng>解除引用到&String&String解除引用到&str&strsplit_at方法。

例如,假设你有以下类型:

  1. struct Selector<T> {
  2. /// Elements available in this `Selector`.
  3. elements: Vec<T>,
  4. /// The index of the "current" element in `elements`. A `Selector`
  5. /// behaves like a pointer to the current element.
  6. current: usize
  7. }

为了使Selector的行为像文档注释所声明的那样,你必须为类型实现Deref和DerefMut:

  1. use std::ops::{Deref, DerefMut};
  2. impl<T> Deref for Selector<T> {
  3. type Target = T;
  4. fn deref(&self) -> &T {
  5. &self.elements[self.current]
  6. }
  7. }
  8. impl<T> DerefMut for Selector<T> {
  9. fn deref_mut(&mut self) -> &mut T {
  10. &mut self.elements[self.current]
  11. }
  12. }

当你应用之后,你能使用Selector如下:

  1. let mut s = Selector { elements: vec!['x', 'y', 'z'],
  2. current: 2 };
  3. // Because `Selector` implements `Deref`, we can use the `*` operator to
  4. // refer to its current element.
  5. assert_eq!(*s, 'z');
  6. // Assert that 'z' is alphabetic, using a method of `char` directly on a
  7. // `Selector`, via deref coercion.
  8. assert!(s.is_alphabetic());
  9. // Change the 'z' to a 'w', by assigning to the `Selector`'s referent.
  10. *s = 'w';
  11. assert_eq!(s.elements, ['x', 'y', 'w']);

DerefDerefMut trait是为实现智能指针类型而设计的,比如Box,RcArc,以及作为你经常使用东西的引用,就像VecString作为[T]str的拥有版本。你不应该为一个类型实现DerefDerefMut,如果只是为了让某些类型的方法能够在目标类型上实现,就像c++基类的方法在子类上可见一样。这并不总是如你所期望的那样成功,当它出错时可能会令人困惑。

deref强制约束带来了一个警告,可能会导致一些混乱:Rust应用它们来解决类型冲突,但不满足类型变量的界限。

例如,以下代码运行良好:

  1. let s = Selector { elements: vec!["good", "bad", "ugly"],
  2. current: 2 };
  3. fn show_it(thing: &str) { println!("{}", thing); }
  4. show_it(&s);

show_it(&s)调用中,Rust看到一个类型&Selector<&str>的参数和一个类型&str的参数,找到Deref实现,并根据需要重写对show_it(s.deref())的调用。

然而,如果你把show_it变成一个泛型函数,Rust突然就不再合作了:

  1. use std::fmt::Display;
  2. fn show_it_generic<T: Display>(thing: T) { println!("{}", thing); }
  3. show_it_generic(&s);

Rust 编译:

截屏2022-06-21 23.44.06.png

这可能会令人困惑:为什么引入泛型会产生错误呢?首先我们要指明, Selector<&str>本身并不实现Display,但它解引用&str&str应用了Display

现在我们来看泛型,因为你正在传递一个类型&Selector<&str>的参数,而函数的参数类型是&T,类型变量T必须是Selector<&str>。然后,Rust检查是否满足了界限T: Display:因为它没有应用deref强制来满足类型变量的界限,这个检查失败了。

要解决这个问题,你可以使用as操作符来说明强制转换:

  1. show_it_generic(&s as &str);

或者,按照编译器的建议,你可以使用&*来强制使用:

  1. show_it_generic(&*s);

Default

有些类型有一个相当明显的默认值:默认向量或字符串都为空,默认数字为0,默认Option为None,等等。以上提到的类型可以实现std::default:: default trait:

  1. trait Default {
  2. fn default() -> Self;
  3. }

默认方法只返回一个Self类型的新值。String的Default实现很简单:

  1. impl Default for String {
  2. fn default() -> String {
  3. String::new()
  4. }
  5. }

Rust的所有集合(collection)类型——vecHashMapBinaryHeap等——都实现了Default,并带有返回空集合的默认方法。当你需要构建一个值集合,但又希望让调用者决定要怎么构建这个集合时,这是很有帮助的。例如,Iterator trait的分区方法将迭代器生成的值分成两个集合,使用闭包来决定每个值的位置:

  1. use std::collections::HashSet;
  2. let squares = [4, 9, 16, 25, 36, 49, 64];
  3. let (powers_of_two, impure): (HashSet<i32>, HashSet<i32>)
  4. = squares.iter().partition(|&n| n & (n-1) == 0);
  5. assert_eq!(powers_of_two.len(), 3);
  6. assert_eq!(impure.len(), 4);

|&n| n & (n-1) == 0是一个闭包,用来通过一些操作来识别2的幂数,.partition操作用来生成两个hashset。当然,.partition不是只能生成hashset;你可以使用它来产生任何你喜欢的集合,但是前提条件是这个collection要实现两个trait:Default,用以初始化一个空的collection;Extend<T>,用以向collection添加<T>类型的元素。

String实现了DefaultExtend<char>,所以你可以写:

  1. let (upper, lower): (String, String)
  2. = "Great Teacher Onizuka".chars().partition(|&c| c.is_uppercase());
  3. assert_eq!(upper, "GTO");
  4. assert_eq!(lower, "reat eacher nizuka");

Default的另一个常见用途是为一些特殊的struct生成默认值,这些struct 的特点是他们通常代表了大量的默认参数(parameters),因此这些struct大多数通常不需要更改。举个例子,有一个名为glium的crate为强大而复杂的OpenGL图形库提供了Rust绑定。glium::DrawParameters是一个struct,包括24个字段(field),每个字段控制OpenGL如何渲染一些图形的不同细节。glium draw函数需要一个DrawParameters结构体作为参数。因为DrawParameters实现了Default,因此你在创建你的个性化参数时,只需要把你想定制的变量修改掉,其他的变量,可以使用default默认来生成。

  1. let params = glium::DrawParameters {
  2. line_width: Some(0.02),
  3. point_size: Some(0.02),
  4. .. Default::default()
  5. };
  6. target.draw(..., &params).unwrap();

我们来看一下上面的代码做了什么。首先调用Default:: Default()创建一个DrawParameters值,初始化所有字段的默认值,然后使用..语法创建了一个新的struct,其中line_widthpoint_size字段是修改过的。之后再将新生成的struct传递给target.draw

如果类型T实现了Default,则标准库自动实现了Rc<T>Arc<T>Box<T>Cell<T>RefCell<T>Cow<T>Mutex<T>RwLock<T>Default。例如,类型Rc<T>的默认值是指向类型T的默认值的Rc。

如果一个元组类型的所有元素类型都实现Default,那么该元组类型也会实现Default,默认为一个包含每个元素默认值的元组。

Rust不会隐式地为sturct类型实现Default,但是如果一个结构的所有字段都实现Default,你可以使用#[derive(Default)]自动为该结构实现Default

AsRef and AsMut

当一个类型实现AsRef<T>时,这意味着你可以有效地从它借用一个&TAsMut类似于可变引用。它们的定义如下:

  1. trait AsRef<T: ?Sized> {
  2. fn as_ref(&self) -> &T;
  3. }
  4. trait AsMut<T: ?Sized> {
  5. fn as_mut(&mut self) -> &mut T;
  6. }

举个例子,Vec<T>实现了AsRef<[T]>,而String实现了AsRef<str>。你也可以借用String的内容作为字节数组,所以String也实现了AsRef<[u8]>

AsRef通常用于使函数在接受的参数类型方面更灵活。例如,std::fs::File::open函数是这样声明的:

  1. fn open<P: AsRef<Path>>(path: P) -> Result<File>

open真正需要的是一个&Path,这是表示文件系统路径的类型。但是有了这个签名,open接受任何它可以借用&Path的东西,也就是任何实现AsRef<Path>的东西。这类类型包括Stringstr,操作系统接口字符串类型OsStringOsStr,当然还有PathBufPath;如果感兴趣,可以查找官方文档寻找更详细的定义。下面是一个例子,能够通过传递字符串来使用open

  1. let dot_emacs = std::fs::File::open("/home/jimb/.emacs")?;

所有标准库的文件系统访问函数都以这种方式接受路径参数。对于调用者来说,这种效果类似于c++中的重载函数,尽管Rust采用了不同的方法来确定哪些参数类型是可接受的。

但这并不是故事的全部。字符串文字是&str,但是实现AsRef<Path>的类型是str,没有&。正如我们之前在DerefDeref‐Mut中解释的那样,Rust不尝试Deref强制约束来满足类型变量边界,所以它们在这里也没有帮助。

幸运的是,标准库包含了blanket实现:

  1. impl<'a, T, U> AsRef<U> for &'a T
  2. where T: AsRef<U>,
  3. T: ?Sized, U: ?Sized
  4. {
  5. fn as_ref(&self) -> &U {
  6. (*self).as_ref()
  7. }
  8. }

换句话说,对于任何类型T和U,如果T: AsRef<U>,那么&T: AsRef<U>也一样:只需遵循引用并像之前那样进行。特别地,因为str: AsRef<Path>,那么&str: AsRef<Path>也一样。在某种意义上,这是一种在检查类型变量的AsRef边界时,获得的deref强制的限制形式。

你可能会假设如果一个类型实现了AsRef<T>,那么它也应该实现AsMut<T>。然而,这个假设并不是永远成立。举个例子,我们提到String实现了AsRef<[u8]>;这是有意义的,因为每个String肯定都有一个字节缓冲区,可以作为二进制数据访问。然而,String进一步保证这些字节是unicode文本格式良好的UTF-8编码;如果String实现了AsMut<[u8]>,这将允许调用者更改String的字节为他们想要的任何内容,并且你不再相信String是格式良好的UTF-8。只有当修改类型为T的元素能够保持类型的一致性时,类型实现AsMut<T>才是有意义的。

AsRefAsMut使用简单的方法,为引用转换提供了标准的、通用的trait,从而避免了更具体的类型转换,以及对应带来繁琐操作。这对你来说能够带来的好处是,你可以实现AsRef<Foo>,而不用自定义地实现AsFoo

Borrow and BorrowMut

std::borrow::borrow特性类似于AsRef:如果一个类型实现了Borrow<T> ,那么它的borrow方法有效地从它借了一个&T。但是Borrow施加了更多的限制:只有当&T散列并与它所借的值散列方式相同时,类型才应该实现Borrow。(Rust没有强制执行;这只是该trait的书面意图。) 这使得Borrow在处理一些值时很有价值:哈希表、树、将要被哈希或者被比较的值。

在借用String时,这种区别很重要,例如String实现了AsRef<str>AsRef<[u8]>,和AsRef<Path>,但这三种trait对应的目标类型(str、u8、Path)通常具有不同的哈希值。只有&str片保证像等效的String一样散列,所以String只实现Borrow<str>

Borrow的定义与AsRef相同;只有名字改变了:

  1. trait Borrow<Borrowed: ?Sized> {
  2. fn borrow(&self) -> &Borrowed;
  3. }

Borrow旨在解决泛型哈希表和其他关联collection类型的特定情况。

例如,假设你有一个std::collections::HashMap<String, i32>,即,将字符串映射到数字。显然这个表的键是Strings;每个记录都拥有一个key。那么我们来思考一下怎么样从这个哈希表中查找具体的一条记录,下面是一种简单的尝试:

  1. impl<K, V> HashMap<K, V>
  2. where K: Eq + Hash
  3. {
  4. fn get(&self, key: K) -> Option<&V> { ... }
  5. }

这是有意义的:要查找条目,必须为表提供适当类型的键。但在这里,K是String;这个签名将迫使我们每次调用都传递一个String,这显然是不合理的。我们稍微改进一下,只需要添加一个键的引用:

  1. impl<K, V> HashMap<K, V>
  2. where K: Eq + Hash
  3. {
  4. fn get(&self, key: &K) -> Option<&V> { ... }
  5. }

这样稍微好一点,但现在你必须将键传递为&String,所以如果你想查找一个常量字符串,你必须这样写:

  1. hashtable.get(&"twenty-two".to_string())

我们来看一下这样实现的细节:

  1. 它在堆上分配了一个String缓冲区,并将文本复制到其中,
  2. 它可以借它作为&String
  3. 传递&Stringget
  4. 丢弃String

显然这也有些荒谬,没有那么完美。

我们应该预想这个方法是完美的,可以传递任何可以哈希并与键类型进行比较的内容;例如,&str应该就足够了。这是最后一个迭代,你可以在标准库中找到:

  1. impl<K, V> HashMap<K, V>
  2. where K: Eq + Hash
  3. {
  4. fn get<Q: ?Sized>(&self, key: &Q) -> Option<&V>
  5. where K: Borrow<Q>,
  6. {...}
  7. }

换句话说,如果你可以借用一个记录的键作为&Q,然后得到的引用按照键本身的方式进行哈希和比较,那么显然&Q应该是一个可接受的键类型。因为String实现了Borrow<str>Borrow<String>,这个最终版本的get允许你根据需要传递&String&str作为键。

Vec<T>[T: N] 应用了Borrow<[T]> 。每个“类字符串”类型都允许借用其对应的切片类型:String实现Borrow<str>PathBuf实现Borrow<Path>,等等。所有标准库的关联集合类型都使用Borrow来决定哪些类型可以传递给它们的查找函数。

标准库包括一个blanket实现,这样每个类型T都可以从自身借用:T: Borrow。这确保了&K始终是在HashMap<K,V>中查找条目的可接受类型。

为了方便起见,每个&mut T类型也实现了Borrow<T>,像往常一样返回一个共享引用&T。这允许你将可变引用传递给collection的查找函数,而不必重新借用共享引用,模拟了Rust通常从可变引用到共享引用的隐式强制。

BorrowMut特征类似于可变引用的Borrow:

  1. trait BorrowMut<Borrowed: ?Sized>: Borrow<Borrowed> {
  2. fn borrow_mut(&mut self) -> &mut Borrowed;
  3. }

Borrow描述的相同期望也适用于BorrowMut

From and Into

std::convert::Fromstd::convert::Into traits表示使用一种类型的值并返回另一种类型的值的转换。而AsRefAsMut特性从另一个类型中借用一种类型的引用,fromInto获得它们的参数的所有权,转换它,然后将结果的所有权返回给调用者。

它们的定义非常对称:

  1. trait Into<T>: Sized {
  2. fn into(self) -> T;
  3. }
  4. trait From<T>: Sized {
  5. fn from(other: T) -> Self;
  6. }

标准库自动实现了从每种类型到自身的简单转换:每种类型T都实现了from<T>Into<T>

虽然这些特性只是提供了两种方法来做同一件事,但它们各自有不同的用途。

通常使用Into使函数在接受参数方面更灵活。例如,如果你这样写:

  1. use std::net::Ipv4Addr;
  2. fn ping<A>(address: A) -> std::io::Result<bool>
  3. where A: Into<Ipv4Addr>
  4. {
  5. let ipv4_address = address.into();
  6. ...
  7. }

那么ping不仅可以接受Ipv4Addr作为参数,还可以接受u32[u8;4]数组,因为这两种类型都方便地实现Into<Ipv4Addr>。(有时将IPv4地址视为单个32位值或4字节数组是有用的。)因为pingaddress唯一知道的是它实现Into<Ipv4Addr>,所以当你调用Into时不需要指定你想要的类型;只有一种类型可能满足条件,所以类型推断将会自动填充它。

与上一节中的AsRef一样,其效果很像c++中的重载函数。通过前面的ping定义,我们可以进行以下调用:

  1. println!("{:?}", ping(Ipv4Addr::new(23, 21, 68, 141))); // pass an Ipv4Addr
  2. println!("{:?}", ping([66, 146, 219, 98])); // pass a [u8; 4]
  3. println!("{:?}", ping(0xd076eb94_u32)); // pass a u32

然而,From特征起着不同的作用。from方法用作泛型构造函数,用于从其他单个值生成类型的实例。例如,Ipv4Addr没有实现两个名字分别为from_arrayfrom_u32的方法,它只是简单地实现了From<[u8;4]>From<u32>,允许我们写入:

  1. let addr1 = Ipv4Addr::from([66, 146, 219, 98]);
  2. let addr2 = Ipv4Addr::from(0xd076eb94_u32);

我们可以让类型推断来分类应用哪个实现。

给定一个合适的From实现,标准库自动实现相应的Into特征。当你定义自己的类型时,如果它有单参数构造函数,你应该将它们编写为适当类型的From<T>的实现;然后你将自动获得相应的Into实现。

由于from和into转换方法拥有参数的所有权,因此新值的构建可以使用原值的资源。例如,假设你写:

  1. let text = "Beautiful Soup".to_string();
  2. let bytes: Vec<u8> = text.into();

Into<Vec<u8>>String的实现简单地接受String的堆缓冲区,并将其重新用作返回的vector的元素缓冲区,不加更改。转换不需要分配空间或复制文本。这是move能够支持的另一种高效实现。

这些转换还提供了一种很好的方式,可以将受约束类型的值放宽为更灵活的值,而不会削弱受约束类型的约束保证。举个例子,String保证其内容始终是有效的UTF-8;它的可变(mutating)方法被小心地限制,以确保我们所做的任何事情都不会引入错误的UTF-8。但是这个例子有效地将一个字符串“降级”为一个纯字节块,你可以对它做任何你想做的事情:也许你要压缩它,或者将它与其他非UTF-8的二进制数据合并。因为into接受它的参数值,文本在转换后不再初始化,这意味着我们可以自由地访问前String的缓冲区,而不会破坏任何现有的String。

然而,简单的转换并不是IntoFrom合同的一部分。AsRefAsMut转换被认为是简单的,相比之下FromInto转换可能会分配、复制或以其他方式处理值的内容。例如,String实现From<&str>,它将字符串片复制到一个新的堆为String分配的缓冲区中。而std::collections::BinaryHeap<T> 应用了 From<Vec<T> >,它根据算法的要求对元素进行比较并重新排序。

?操作符使用FromInto来帮助清理可能以多种方式失败的函数中的代码,在需要时自动将特定错误类型转换为一般错误类型。

例如,想象一个系统需要读取二进制数据,并将其中的一部分从十进制的数字转换成UTF-8文本。这意味着要使用std::str::from_utf8i32FromStr实现,它们都可以返回不同类型的错误。假设我们使用在讨论错误处理时定义的GenericErrorGenericResult类型,则?操作符将为我们进行转换:

  1. type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>;
  2. type GenericResult<T> = Result<T, GenericError>;
  3. fn parse_i32_bytes(b:&[u8]) -> GenericResult<i32> {
  4. Ok(std::str::from_utf8(b)?.parse::<i32>()?)
  5. }

像大多数错误类型一样,Utf8ErrorParseIntError实现了Error特征,标准库为我们提供了一个blanket From impl,对于一些特殊的转换:这些转换上应用了Error或者是一个Box<dyn Error>,在这些转换上,操作符能够自动使用

  1. impl<'a, E: Error + Send + Sync + 'a> From<E>
  2. for Box<dyn Error + Send + Sync + 'a> {
  3. fn from(err: E) -> Box<dyn Error + Send + Sync + 'a> {
  4. Box::new(err)
  5. }
  6. }

这将一个具有两个match语句的相当大的函数变成一个单行函数。

在将FromInto添加到标准库之前,Rust代码充满了单跳的转换特性和构造方法,也就是说,每一个类型转换和构造方法都是特定于单个类型的。FromInto编码惯例,可以是自定义的类型更容易使用,因为代码的使用者已经熟悉它们。其他库和语言本身也可以依赖这些特征作为规范的、标准化的方式来编码转换。

FromInto是绝对可靠的特性——它们的API要求转换不会失败。

不幸的是,许多转换比这更复杂。例如,像i64这样的大整数可以存储比i32大得多的数字,而将像2_000_000_000_000i64这样的数字转换为i32没有一些额外的信息是没有意义的。做一个简单的位转换,抛出前32位,通常不会产生我们希望的结果:

  1. let huge = 2_000_000_000_000i64;
  2. let smaller = huge as i32; println!("{}", smaller); // -1454759936

处理这种情况有很多选择。根据上下文的不同,这样的“包装”转换可能是合适的。另一方面,数字信号处理和控制系统等应用程序通常可以使用“饱和”转换,即大于可能最大值的数字被限制为该最大值。

TryFrom and TryInto

从名称就可以推断出FromIntoTryFromTryInto的关系,我们前面提到,FromInto是完全可靠的操作,所以当我们不确定是否能使用FromInto时,我们可以使用TryFromTryInto

举个例子,由于Rust不清楚这样的转换应该如何表现,Rust没有对于i32实现From ,或者任何其他会丢失信息的数值类型之间的转换。相反,i32实现TryFromTryFromTryIntoFromInto容易犯错的表亲,TryFromTryInto同样是互惠的;实现TryFrom意味着也实现了TryInto

它们的定义只比From和Into稍微复杂一点。

  1. pub trait TryFrom<T>: Sized {
  2. type Error;
  3. fn try_from(value: T) -> Result<Self, Self::Error>;
  4. }
  5. pub trait TryInto<T>: Sized {
  6. type Error;
  7. fn try_into(self) -> Result<T, Self::Error>;
  8. }

try_into()方法给我们一个Result,因此我们可以选择在特殊情况下要做什么,例如一个数字太大而无法在结果类型中容纳:

  1. use std::convert::TryInto;
  2. // Saturate on overflow, rather than wrapping
  3. let smaller: i32 = huge.try_into().unwrap_or(i32::MAX);

如果我们也想处理负数的情况,可以使用Result的unwrap_or_else()方法:

  1. let smaller: i32 = huge.try_into().unwrap_or_else(|_|{
  2. if huge >= 0 {
  3. i32::MAX
  4. } else {
  5. i32::MIN
  6. }
  7. });

为自己的类型实现容易出错的转换也很容易。Error类型可以是简单的,也可以是复杂的,这取决于特定应用程序的要求。标准库使用一个空结构体,除了错误发生之外没有提供任何信息,因为唯一可能的错误是溢出。另一方面,更复杂类型之间的转换可能需要返回更多信息:

  1. impl TryInto<LinearShift> for Transform {
  2. type Error = TransformError;
  3. fn try_into(self) -> Result<LinearShift, Self::Error> {
  4. if !self.normalized() {
  5. return Err(TransformError::NotNormalized);
  6. }
  7. ...
  8. }
  9. }

其中FromInto用简单的转换将类型联系起来,TryFromTryInto用Result提供的具有表现力的错误处理扩展了FromInto转换的简单性。这四种特性可以在一个crate中关联多种类型。

ToOwned

对于给定的引用,生成其引用所属副本的通常方法是调用Clone,假设类型实现了std::clone::Clone。但是如果想要克隆&str&[i32]呢?你可能想要的是一个StringVec<i32>,但Clone的定义不允许这样做:根据定义,克隆一个&T必须总是返回一个类型T的值,并且str[u8]unsized的;它们甚至不是函数可以返回的类型。

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

  1. trait ToOwned {
  2. type Owned: Borrow<Self>;
  3. fn to_owned(&self) -> Self::Owned;
  4. }

不像Clone必须完全返回Self,to_owned可以返回任何你可以从其中借到&Self的东西:Owned类型必须实现borrow<Self> 。你可以从Vec<T>中借用一个&[T],所以[T]可以实现ToOwned<<Owned=Vec<T>> >,只要T实现了Clone,这样我们就可以将切片的元素复制到vector中。类似地,str实现ToOwned<Owned = String>Path实现ToOwned<Owned = PathBuf>,依此类推。

Borrow and ToOwned At Work: The Humble Cow

如果你想熟练掌握Rust,你不得不考虑所有权的问题,比如函数应该通过引用还是通过值接收参数。通常你可以选择是引用还是通过值,你的决定会直接反应在函数的参数中。但在某些情况下,在程序运行之前,你不能决定是借用还是拥有;std::borrow::Cow类型(用于“写时克隆”)提供了一种方法。

它的定义如下:

  1. enum Cow<'a, B: ?Sized>
  2. where B: ToOwned
  3. {
  4. Borrowed(&'a B),
  5. Owned(<B as ToOwned>::Owned),
  6. }

一个Cow<B>要么借用了一个对B的共享引用,要么拥有一个我们能从中引用的值。因为Cow实现了Deref,你可以调用它的方法,就像它是B的共享引用一样:如果它是Owned,它借用了对Owned值的共享引用;如果它是Borrowed,它只是传递它持有的引用。

你也可以通过调用它的to_mut方法来获得一个对Cow值的可变引用,它会返回一个&mut B。如果Cow恰好是Cow:: borrowedto_mut简单地调用引用的to_owned方法来获得它自己的引用,把它变成Cow::Owned,并借用一个对新拥有的值的可变引用。这就是类型名称所指的“写时克隆”行为。

类似地,Cow也有一个into_owned方法,它在必要时将引用提升到一个owned值,然后返回该值,将所有权转移到调用者,并在此过程中消耗Cow

Cow的一个常见用途是返回一个静态分配的字符串常量或一个计算出来的字符串。例如,假设你需要将一个错误枚举转换为信息(message)。大多数变体可以用固定的字符串处理,但其中一些变体具有应该包含在消息中的额外数据。你可以返回一个Cow<'static, str>:

  1. use std::path::PathBuf;
  2. use std::borrow::Cow;
  3. fn describe(error: &Error) -> Cow<'static, str> {
  4. match *error {
  5. Error::OutOfMemory => "out of memory".into(),
  6. Error::StackOverflow =>"stack overflow".into(),
  7. Error::MachineOnFire => "machine on fire".into(),
  8. Error::Unfathomable => "machine bewildered".into(),
  9. Error::FileNotFound(ref path) => {
  10. format!("file not found: {}", path.display()).into()
  11. }
  12. }
  13. }

这段代码使用CowInto实现来构造值。这个match语句的大多数分支返回一个Cow::Borrowed引用一个静态分配的字符串。但是当我们得到一个FileNotFound变量时,我们使用format!构造包含给定文件名的消息。match语句的这个分支产生一个Cow::Owned值。

不需要改变值的describe调用者可以简单地将Cow视为&str:

  1. println!("Disaster has struck: {}", describe(&error));

需要拥有值的调用者可以很容易地生成一个:

  1. let mut log: Vec<String> = Vec::new();
  2. ...
  3. log.push(describe(&error).into_owned());

使用Cow有助于describe,它的调用者将分配推迟到必要的时候。