简介
trait是rust中的一种抽象机制和约束机制,类似于其他语言的interface。更重要的是,trait作为统一的建模元素,贯穿了Rust语言,从语义实现到第三方库,trait的身影无处不在。
trait提供如下功能:
- 从不同类型抽象出共同的方法集合, 为程序提供多态能力。
- trait可以约束泛型和trait,如:
trait T1: T2
, 表明实现T1必须要先实现trait T2struct<T: T2> Obj(T);
, 表明类型T必须实现T2.
如下是一个trait的声明:
trait Animal{
// associated type
type Output;
// constants
const zhi: i32 = 10;
// default impl
fn park(&self){
println!("wang!");
}
// Self is the type that impl Animal trait.
fn method() -> Self;
}
impl Animal for Type{
// ...
}
一个使用trait的例子:
// Programming Rust, page 260
fn dot<N>(v1: &[N], v2: &[N]) -> N
where N: Add<Output=N> + Mul<Output=N> + Default + Copy
{
for i in 0 .. v1.len() {
total = total + v1[i] * v2[i];
}
total
}
这个函数有一个泛型参数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信息传递给底层引擎,通过逻辑推演进行判断。
如下面的例子:
// &T实现了Copy, 所以共享引用是复制语义的。
impl<T: ?Sized> Clone for &T {
#[inline]
fn clone(&self) -> Self {
*self
}
}
// 可变引用不能被复制
impl<T: ?Sized> !Clone for &mut T {}
// 任何类型的常量指针 *const T(原生不可变指针), 都不是Send,
// 也就意味*const T不能在跨线程移动。
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> !Send for *const T {}
常见的Trait
auto trait
指的是std中由编译器自动实现的一系列trait。如:
pub unsafe auto trait Send {
// empty.
}
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的关系:
例如,假设 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参数。
- 条件1,方法必须被
定义看起来繁琐,但只要牢记根本原因: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无法调用它。
为什么不能包含生命周期以外的泛型参数?
因为泛型是编译时展开替换为具体类型,trait object唯一拥有的信息就是对象指针data与vtable pointer, 要使用泛型,除非把泛型类型信息写入trait object中,这会导致trait object体积膨胀,Rust直接禁止了事。
禁止trait const的原因也应该相同。
参考资料:
- https://zhuanlan.zhihu.com/p/23791817
- https://doc.rust-lang.org/std/raw/struct.TraitObject.html?search=
- Trait object
- object safety
impl trait
a: impl T
表示a实现了某个trait,这样的表达式只能出现在入参和返回值中。
但是笔者产生了疑惑:这跟用泛型约束有什么区别?如下是入参对比:
fn func2(a: impl Animal){
a.park();
}
fn func1<T: Animal>(a: T){
a.park();
}
基本一样。那作为返回值呢,有什么不同?
// 2015 edition, return Trait cause compiling error.
// Use Box<dyn Trait> instead in the cost of dynamic dispacthing and heap allocation.
fn new_animal_2015() -> Box<dyn Animal>{
Cat
}
fn new_animal() -> impl Animal{
Cat
}
fn new_animal_generic<T: Animal>() -> T{
T::defalut()
}
在Rust 2018前,不能用 -> Trait
这种语法,编译器要求返回具体类型的实例。要么用Box封装,要么返回泛型。总的来说,这两者的语义有微妙差别,但能够达到相同目的。
a: impl T
表示a满足T,但a不是泛型,是某个具体类型。A: impl T
表示泛型A满足T,调用者需要指定\由编译器自动推断出A的类型, 然后在编译时被替换为具体类型。
新版本下,如果要表达实现了某trait这一约束这个语义,直接用新语法即可,无需引入泛型。
参考:
dyn trait
歧义: Box<T>
,T是一个类型还是trait?
解决方法:
Box<dyn T> // T是trait
Box<T> // T是类型
Async function in trait
目前Rust(1.47)还不能支持如下语法:
trait AsyncFunc{
async fn f(&self) -> Self;
// 自动转换
// fn f(&self) -> Pin<Box<dyn Future<Output = User> + Send + '_>>;
}
要使用 aynsc-trait这个第三方库.
“why async fn in traits are hard”一文中指出如果要在trait中支持async函数,可能面临一些问题,涉及GAT以及编码复杂度:
- 异步函数返回值会封装到实现Future结构体中,如果参数有引用,那么还要带上生命周期参数,也就导致了关联类型必须是泛型关联类型(GAT), 目前rust不支持GAT。
- 现有的语法表达“返回的Future满足某trait”比较繁琐,添加新语法糖可能是更好选择。
参考资料: