Rust 中的 Trait 有点像其它语言的接口,不过实际上略有不同。
简介
如下是一个简单的示例,包含了模板以及 trait 相关,使用模板时可以进行约束,如果比较简单那么可以通过 <T: XXX + YYY> 的方式指定,或者采用类似如下的方式,通过 where 进行声明。
trait Shape<T> {
fn area(&self) -> T;
}
struct Rectangle<T> {
width: T,
height: T,
}
impl<T> Shape<T> for Rectangle<T>
where
T: std::ops::Mul<Output = T> + Copy,
{
fn area(&self) -> T {
self.width * self.height
}
}
fn main() {
let rect = Rectangle {
width: 30,
height: 50,
};
println!("The area is {}", rect.area());
}
孤儿规则
当为某类型实现某个 Trait 时,必须要保证类型或者 Trait 至少有一个在当前作用域中定义,通过这个规则保证其它人使用你的库时不会破坏库中的函数逻辑,也就是所谓的孤儿规则 (Orphan Rule)。简单来说,允许如下操作:
- 自定义类型实现外部的
trait,这也是最常用,例如实现标准库的DisplayDefault等。 - 对外部类型实现自定义的
trait,从而某种类型可以支持自定义的实现。
注意,当实现了三方的 trait 后,在使用时,需要同时将对应 trait 引入,尤其是某些场景下不是直接使用的。通过这种方式,防止重名 trait 发生歧义,否则会有类似如下的报错。
error[E0599]: the method `map` exists for struct `xxx`, but its trait bounds were not satisfied
... ...
= help: items from traits can only be used if the trait is in scope
动态转发
Rust 中的转发支持两种:A) 基于泛型的静态派发,在编译阶段完成类型的确定以及替换,运行性能相对要好;B) 基于 trait object 的动态派发,在运行时确定,有些场景下必须要使用。
为了区分两者,在 RFC 2113 中新增了 dyn 关键字,主要是为了更加明显的标识 trait object 。
trait Shape {
fn area(&self) -> f64;
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
const PI: f64 = 3.1415926;
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
self.radius * self.radius * PI
}
}
fn get_area_box(s: Box<dyn Shape>) -> f64 {
s.area() // auto deref
}
fn get_area(s: &dyn Shape) -> f64 {
s.area()
}
fn main() {
let rect = Rectangle {
width: 10.0,
height: 2.0,
};
println!(
"Rectangle {}, Circle {}",
get_area(&rect),
get_area_box(Box::new(Circle { radius: 3.0 }))
);
}
如果直接取指针就是普通指针,大小是 64bit,当手动通过 as 转换为 trait object 后就成了胖指针,会携带额外信息,也就是所谓的虚函数表,包含了具体的调用地址信息。
use std::mem;
trait Shape {
fn area(&self) -> f64;
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
const PI: f64 = 3.1415926;
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
self.radius * self.radius * PI
}
}
fn dump_trait(p: &dyn Shape) {
let (data, vtable) : (usize, usize) = unsafe {mem::transmute(p)}; // 强制获取内部数据
println!("TraitObject [data:{:#x}, vtable:{:#x}]", data, vtable);
unsafe {
let v: *const usize = vtable as *const () as *const usize;
println!("data in vtable [{:#x}, {:#x}, {:#x}, {:#x}]", *v, *v.offset(1), *v.offset(2), *v.offset(3));
}
}
fn main() {
let rect = Rectangle{width: 10.0, height: 2.0};
let p_rect = ▭ // 8Bytes
let p_shape = p_rect as &dyn Shape; // 16Bytes
println!("sizeof(rect) {}, sizeof(shape) {}", mem::size_of_val(&p_rect), mem::size_of_val(&p_shape));
println!("Circle::area {:#x}", Circle::area as usize);
println!("Rectangle::area {:#x}", Rectangle::area as usize);
dump_trait(&rect);
}
通过如上方式可以将虚函数表打印出来。
impl trait
这是在 Rust 1.26 版本中引入的,可以用在函数的参数和返回值,主要是简化 trait 的使用,算是范型的特例版,因为这些地方都是静态派发。
fn get_area_impl(s: impl Shape) -> f64 {
s.area()
}
get_area_impl(react);
get_area_impl(circle);
如下的方式采用的时动态派发,所以编译时会报错。
fn get_area_impl_deps(i: i32) -> impl Shape {
if i > 10 {
return react;
}
circle
}
Object Safe
并不是所有的 trait 都可用作 trait object 使用,需要满足 object safety 属性才可以,主要有以下几点:
- 函数不能返回
Self,因为对象转为trait object后,原始类型信息就丢失了,所以这里的Self也就无法确定了。 - 函数中不允许有范型参数,主要原因在于单态化时会生成大量的函数,很容易导致
trait内的方法膨胀。 trait不能继承Sized,继承Sized会同时要求trait object也是Sized,而trait object是DST类型。
其它
导入 Trait
Rust 中有个规则,如果要使用 trait 中的方法,需要将对应的 trait 引入到当前 Scope 中。
这主要是因为,很多的 trait 存在签名冲突,此时,某个对象调用相应方法时无法确定如何选择,所以,就简单使用当前 Scope 中的 trait 方法。例如,如下的几个标准库 trait 都包含相同的 fmt 函数签名。
pub trait Debug {
fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}
pub trait Display {
fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}
pub trait LowerHex {
fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}
例如如下的示例需要导入 std::fmt::Write 才可以。
use std::collections::HashMap;
use std::fmt::Write;
fn main() {
let mut properties = HashMap::new();
properties.insert("hoodies.test", "foobar");
properties.insert("hoodies.env", "test");
let mut output = String::new();
write!(output, "{:?}", properties).unwrap();
println!("{}", output);
}
在实现时对类型进行了约束,需要支持 std::ops::Mul 的 trait,而且输入和输出的数据类型是相同的,所以实现 std::ops::Mul<Output = T> 的 trait 即可。
常用技巧
如上已经介绍了泛型特征 Generic Traits、类型关联特征 Associated Type Traits,还有如下的常见用法:
- 特征是可以支持继承的。
Marker Traits在std::marker模块中定义,不包含任何方法,用于获取一些编译期间的保障。- 使用时存在两个比较特殊的生命周期声明:A)
'static整个应用生命周期;B)'_编译过程自动推导。