Rust Trait 详解

2023-10-16 language rust

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());
}

孤儿规则 Orphan Rule

当为某类型实现某个 Trait 时,必须要保证类型或者 Trait 至少有一个在当前作用域中定义,通过这个规则保证其它人使用你的库时不会破坏库中的函数逻辑。

简单来说,允许如下操作:

  • 自定义类型实现外部的 trait,这也是最常用,例如实现标准库的 Display Default 等。
  • 对外部类型实现自定义的 trait,从而某种类型可以支持自定义的实现。

注意,当实现了三方的 trait 后,在使用时,需要同时将对应 trait 引入,尤其是某些场景下不是直接使用的。通过这种方式,防止重名 trait 发生歧义。

动态转发

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 = &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 objectDST 类型。

其它

导入 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);
}