使用trait里的方法的时候必须用use语句写明引用的trait,因为任何人都可以使用trait给类型添加方法,不同trait添加的方法可能重名,所以写明trait可以避免冲突。

trait对象和泛型函数

trait对象(trait object)

写作 dyn T其中T是一个trait,但trait object不能这么用,因为T的大小是不知道的,所以在把他赋给变量的时候,编译器无法直到给这个值分配多少内存。所以trait object要和引用一起用,let writer: &mut dyn Write = &mut buf;因为buf的类型不会在编译的时候确定,所以trait object要额外包含关于buf的类型信息,在调用Write相关函数的时候要在运行时根据类型动态的调用buf的关于Write的实现(dynamic dispatch)。
所以在内存里trait object是一个胖指针,包含所指向的值的地址和一个代表类型的表(virtual table)
image.png

在接受trait object的函数里传入引用会被自动转成trait object。
其它类型的指针也可以用作trait object:Box<dyn Write>Rc<dyn Write>

选择trait对象的好处:

  • 如果想将不同类型的值放到一起,使用Box<dyn T>每个Box都是相同的两个机器字的大小。
  • 可以减小编译后的大小,使用泛型会把每种用到的类型的实现都编译出来,这样一个函数就多了好多份。

选择泛型的好处:

  • 速度快,泛型实现在编译时,编译器就根据用到的类型实现了具体的函数,因为直到了具体的实现,就可以进行更充分的优化。
  • 不是所有的trait都支持trait object。
  • trait object不支持多种trait的累加。

Using trait objects is also known as type erasure. Rust is no longer aware that an
error has originated upstream. Using Box as the error variant of a Result
means that the upstream error types are, in a sense, lost. The original errors are now
converted to exactly the same type.

trait的定义和实现

trait的实现代码块impl Trait for Type里只能出现trait里定义的函数,如果想写一些辅助的函数也写到普通的impl Type {}里。

orphan rules:

trait和type必须必须至少有一个是在当前crate的,也就是说不能给别的crate的type实现别的crate引入的trait。这么做的原因是因为如果允许这么做,相同的trait和type可能会在不同的crate有多个实现,而如果这两个crate被同事使用的时候,就不知道应该用哪个实现了。

object safe

trait里函数的参数和返回值的类型也可以是Self,但使用了self的函数不能用trait object调用,因为用trait object的主要原因是参数的类型在编译时是不能确定的,这样编译器也无法检查调用的类型是否正确:无法验证trait object代表的类型是否和Self一致。

  1. for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>rustcE0038

subtrait

trait Creature: Visible {}要实现Creature的类型必须是实现了Visible的。Creature是Visible的subtrait,反之是supertrait。
subtrait并不继承supertrait的关联项,如果想要使用supertrait的方法则必须引入supertrait。
实际上subtrait的写法是一种简写,完整就是trait Creature where Self: Visible {}
多个supertrait:trait Dup: Clone + Copy {}

trait也可以有静态方法(type-associated function)。

qualified method call

普通的方法调用方式"hello".to_string()其实是省略了很多信息的,这里的to_string方法其实是 值"hello"所归属的类型str,实现了trait ToString的方法to_string。也就是有四个参与者:值,类型,trait和方法。rust在编译的时候会补全这些,写的时候也可以自己补全:

  1. str::to_string("hello"),补充了类型,虽然这个方法的第一个参数是self,但这种调用方法就把第实参作为self了。
  2. ToString::to_string("hello"),补充了trait。
  3. <str as ToString>::to_string("hello"),四个参与者全都有了。

这三种都是qualified method call,第三种是fully qualified method call。
使用qualified method call的地方:

  • 一个类型实现了多个trait的同名的方法。
  • 值的类型被省略了,编译器无法推测出类型。
  • 方法本身作为函数值了:line.split_whitespace().map(ToString::to_string)
  • 在宏里调用了方法。

关联函数也可以使用qualified调用方式。

关联类型(associate type)

在trait中增加一个额外的类型type Item;

  1. pub trait Iterator {
  2. type Item;
  3. fn next(&mut self) -> Option<Self::Item>;
  4. ...
  5. }

在实现trait的时候要将该类型给定具体的类型

  1. // (code from the std::env standard library module)
  2. impl Iterator for Args {
  3. type Item = String;
  4. fn next(&mut self) -> Option<String> {
  5. ...
  6. }
  7. ...
  8. }

使用泛型函数时就可以指明参数或返回类型里的关联类型

  1. /// Loop over an iterator, storing the values in a new vector.
  2. fn collect_into_vector<I: Iterator>(iter: I) -> Vec<I::Item>
  3. // 也可以指明关联类型需要实现的trait
  4. fn dump<I>(iter: I) where I: Iterator, I::Item: Debug
  5. // 也可以指明关联类型为某个具体的类型
  6. fn dump<I>(iter: I) where I: Iterator<Item=String>
  7. // 可以用在trait 对象里
  8. fn dump(iter: &mut dyn Iterator<Item=String>)

Iterator<Item=String>本身也是一个trait,可以看做是Iterator的一个子集。

泛型trait

  1. pub trait Mul<RHS=Self> {
  2. ...
  3. }

Self是泛型参数RHS的默认值,实现的时候如果不定义,默认值就被带入。

但是泛型trait和关联类型的区别是什么?下面两种实现可以有相同的效果。so上这个回答说:关联类型强调一种类型的实现只能有一种关联类型,泛型trait可以有多种类型的实现。比如这里的Mul,一个类型可以和多种类型都有相乘的操作,上面的Args类型的迭代输出只能是String一种类型。

impl Trait

impl Trait可以用来简化类型的书写,只有在确定类型是唯一的时候才能用这种简化。

  1. trait Trait {}
  2. // argument position
  3. fn foo(arg: impl Trait) {
  4. }
  5. fn foo<T: Trait>(arg: T) {
  6. }
  7. // return position
  8. fn foo() -> impl Trait {
  9. }
  10. fn foo() -> Box<dyn Trait> {
  11. }

用在参数的形式的时候,普通的类型绑定可以让调用者在调用的时候指定该函数用在什么类型上,但impl Trait的形式不能指定。在返回类型的形式中,之所以能代替trait object也是因为返回的类型只可能有一种,而函数的调用者只是需要返回类型是实现了某个Trait的。

好处:

  • 因为知道返回类型只可能是一种,所以编译器可以确定需要分配多少内存,就不用trait object的overhead。
  • 使用impl Trait的好处还有函数的返回类型可以修改只要仍然实现返回类型指定的Trait就不会打破调用者的旧代码。

坏处:

  • 也因为在编译时类型必须确定,所以Trait方法是不能用的。
  • 泛型方法中因为没有给定一个类型参数,所以函数内想要使用的该类型的时候没法写出来。

关联常量

  1. trait Greet {
  2. const GREETING: &'static str = "Hello";
  3. fn greet(&self) -> String;
  4. }

也可以不给值,这样实现的时候就必须给定值。使用的时候用类型调用:T::GREETING。trait object不能用关联常量,因为其值必须通过具体类型的实现来确定,而常量是必须在编译的时候确定的。