简介

trait是rust中的一种抽象机制和约束机制,类似于其他语言的interface。更重要的是,trait作为统一的建模元素,贯穿了Rust语言,从语义实现到第三方库,trait的身影无处不在。

trait提供如下功能:

  • 从不同类型抽象出共同的方法集合, 为程序提供多态能力。
  • trait可以约束泛型和trait,如:
    • trait T1: T2 , 表明实现T1必须要先实现trait T2
    • struct<T: T2> Obj(T); , 表明类型T必须实现T2.

如下是一个trait的声明:

  1. trait Animal{
  2. // associated type
  3. type Output;
  4. // constants
  5. const zhi: i32 = 10;
  6. // default impl
  7. fn park(&self){
  8. println!("wang!");
  9. }
  10. // Self is the type that impl Animal trait.
  11. fn method() -> Self;
  12. }
  13. impl Animal for Type{
  14. // ...
  15. }

一个使用trait的例子:

  1. // Programming Rust, page 260
  2. fn dot<N>(v1: &[N], v2: &[N]) -> N
  3. where N: Add<Output=N> + Mul<Output=N> + Default + Copy
  4. {
  5. for i in 0 .. v1.len() {
  6. total = total + v1[i] * v2[i];
  7. }
  8. total
  9. }

这个函数有一个泛型参数N,要求N是可乘、可加,而且具有default方法。其中 Add<Output=N> 表示 N+N 返回值的类型也是N, Mul 同理。
加上 Copy 约束是因为 v1[i] 会move值, 而v1是一个引用导致不允许move,所以需要 Clone , 使用 Copy 可以隐式clone。

多数Rust语言自身的语义也是基于trait来构建,例如:

  • 编译器根据闭包定义自动生成匿名结构体,并且为其实现 Fn, FnMut, FnOnce 三种trait
  • 通过 Send, Sync 来限制多线程共享变量
  • &T, &mut T 底层也是通过trait来实现的

通过Trait这种方式书写的代码,带有很明确的语义信息,我们可以很方便地用逻辑推理的形式来推导它的性质。
事实上,Rust trait 系统就是通过chalk这个library来实现的,chalk会把trait信息传递给底层引擎,通过逻辑推演进行判断。

如下面的例子:

  1. // &T实现了Copy, 所以共享引用是复制语义的。
  2. impl<T: ?Sized> Clone for &T {
  3. #[inline]
  4. fn clone(&self) -> Self {
  5. *self
  6. }
  7. }
  8. // 可变引用不能被复制
  9. impl<T: ?Sized> !Clone for &mut T {}
  10. // 任何类型的常量指针 *const T(原生不可变指针), 都不是Send,
  11. // 也就意味*const T不能在跨线程移动。
  12. #[stable(feature = "rust1", since = "1.0.0")]
  13. impl<T: ?Sized> !Send for *const T {}

常见的Trait

auto trait 指的是std中由编译器自动实现的一系列trait。如:

  1. pub unsafe auto trait Send {
  2. // empty.
  3. }

impl !trait for T 表示不为T实现trait。某些类型语义上不支持这种trait,那就需要禁止为其自动生成。

有些trait被标记上unsafe,实现时要用 unsafe impl .

Sized 表示类似T是编译期长度已知的类型。例如 [u8; 32] 是Sized的, 而 [u8] 只知道它元素的类型而不知道其长度是多少,所以是不定长类型的, &[u8 是Sized的,只占用8Byte。

?Sized ,默认情况下: 编译器认为T 是Sized的, ?Sized 表示定长或者不定长的。

Clone: Sized ,可以通过显式调用 .clone() 进行复制。

Copy: Clone ,赋值、传参时隐式复制变量。其他语言如C++,默认情况是复制语义,而在Rust中则相反,Copy语义要显示实现Copy语义。

Send 表示这个类型可以在多线程中move。 *const T, Rc 等不是Send的。

Sync 表示这个类型可以安全地在多线程中传递不可变引用,等价于 &T is Send

Display, Debug 可以用于格式化字符串,分别对应 {}, {:?} 占位符,此外还有打印地址的 {:p} 等。

Deref, DerefMut 提供自动解引用。一般由智能指针类型(如 Box<T>, Rc<T>, RefCell<T> )等实现,隐式解引用。

Drop 非常重要的trait,起到析构函数作用。Rust采用RAII内存管理方式,一旦对象结束生命周期,其所拥有的值会自动调用 drop 方法做清理工作。

AsRef, AsMut ,作用是是由T提供 &T, &mut T 。如 AsRef<[u8]> 就表示 T可以表示&[u8] ,这在很多底层相关的trait中很常见。

Borrow, BorrowMut 类似AsRef, AsMut, 但不同在于Borrow一般用于hash和tree等数据结构中,所以接受者类型T应该可以提供hash方法并且以比较,不过这只是在文档注释中声明,没有trait约束。

From, Into 类型转换相关。如 A -> B , 那么可以用 B::from(a) 或者 a.into::<B>()

Iterator, IntoIterator, FromInterator 是与迭代相关的trait。 Iterator 表示接受者可以进行迭代操作, 每次迭代产出 Option<Self::Output> ,如果为None则终止迭代。 IntoIterator 表示可以消费接受者,将其转变成一个迭代器;最后一个表示可以从一个迭代器构造接受者。

Trait Object & Object Safety

Trait Object是什么

泛型、trait可以实现静态分发,那么Rust如何提供动态分发?C++中通过虚函数+vtable实现的,Rust通过Trait Object实现。

Rust中动态分发的内存布局与C++不同:C++的vtable存储在每一个类实例中,而Rust中类型本身没有vtable,vtable只存储在Trait Object。 Trait object是一个16Byte的胖指针,内部只包含了指向接受者的data指针和vtable指针。

那些是对应trait A的trait object呢?

  • Box<dyn A>
  • &dyn A, &mut dyn A, &dyn A<Output=ConcreteType>

下图是Trait object与trait的关系:
image.png
例如,假设 Trait A 是object safe的,那么 Box<dyn A + Send + Sync + 'static> 就是A的一种Trait Object。

什么是Object Safety?

首先,一个Trait A满足Object Safety,等价于同时满足下面条件:

  • A没有被 Self: Sized 约束(通过这个语法来防止生成对应的Trait Object)
  • A内没有定义关联常量。
  • 如果A:B, B也要是object safe
  • 所有方法满足以下条件之一:
    • 条件1,方法必须被 Where Self: Sized 约束(这种方法无法放入trait object)
    • 条件2,同时满足以下条件:
      • 除了lifetime参数外,没有泛型参数。
      • 除了方法的接受者可以用Self,其他不能使用Self参数。

定义看起来繁琐,但只要牢记根本原因:trait object只保留了vtable和对象的指针,没有保留类型信息,因此某些操作无法进行。例如trait的方法不能使用Self作为接受者(self参数)外的参数,因为无法得知Self对应哪个具体类型。

Self: Sized 起到什么作用?

举一个例子帮助理解, 如 fn new() -> Self , 这是一种常见的实现,很明显它不满足trait object。假设trait有一部分是object safe,一部分不是,又想可以产生 Box<dyn trait> , 那要怎么办?

需要一种语法指定某些方法不放入vtable,代价是trait object没法调用这些方法。刚好 RFC 0546提到Self默认不是Sized,那么复用这个Where Self: Sized 语法。

如下图, func3() 不在vtable中,因此trait object无法调用它。
image.png

为什么不能包含生命周期以外的泛型参数?

因为泛型是编译时展开替换为具体类型,trait object唯一拥有的信息就是对象指针data与vtable pointer, 要使用泛型,除非把泛型类型信息写入trait object中,这会导致trait object体积膨胀,Rust直接禁止了事。

禁止trait const的原因也应该相同。

参考资料:

  1. https://zhuanlan.zhihu.com/p/23791817
  2. https://doc.rust-lang.org/std/raw/struct.TraitObject.html?search=
  3. Trait object
  4. object safety

impl trait

a: impl T 表示a实现了某个trait,这样的表达式只能出现在入参和返回值中。

但是笔者产生了疑惑:这跟用泛型约束有什么区别?如下是入参对比:

  1. fn func2(a: impl Animal){
  2. a.park();
  3. }
  4. fn func1<T: Animal>(a: T){
  5. a.park();
  6. }

基本一样。那作为返回值呢,有什么不同?

  1. // 2015 edition, return Trait cause compiling error.
  2. // Use Box<dyn Trait> instead in the cost of dynamic dispacthing and heap allocation.
  3. fn new_animal_2015() -> Box<dyn Animal>{
  4. Cat
  5. }
  6. fn new_animal() -> impl Animal{
  7. Cat
  8. }
  9. fn new_animal_generic<T: Animal>() -> T{
  10. T::defalut()
  11. }

在Rust 2018前,不能用 -> Trait 这种语法,编译器要求返回具体类型的实例。要么用Box封装,要么返回泛型。总的来说,这两者的语义有微妙差别,但能够达到相同目的。

  • a: impl T 表示a满足T,但a不是泛型,是某个具体类型。
  • A: impl T 表示泛型A满足T,调用者需要指定\由编译器自动推断出A的类型, 然后在编译时被替换为具体类型。

新版本下,如果要表达实现了某trait这一约束这个语义,直接用新语法即可,无需引入泛型。

参考:

dyn trait

歧义: Box<T> ,T是一个类型还是trait?
解决方法:

  1. Box<dyn T> // T是trait
  2. Box<T> // T是类型

Async function in trait

目前Rust(1.47)还不能支持如下语法:

  1. trait AsyncFunc{
  2. async fn f(&self) -> Self;
  3. // 自动转换
  4. // fn f(&self) -> Pin<Box<dyn Future<Output = User> + Send + '_>>;
  5. }

要使用 aynsc-trait这个第三方库.

“why async fn in traits are hard”一文中指出如果要在trait中支持async函数,可能面临一些问题,涉及GAT以及编码复杂度:

  • 异步函数返回值会封装到实现Future结构体中,如果参数有引用,那么还要带上生命周期参数,也就导致了关联类型必须是泛型关联类型(GAT), 目前rust不支持GAT。
  • 现有的语法表达“返回的Future满足某trait”比较繁琐,添加新语法糖可能是更好选择。

参考资料: