Trait对象和泛型

对比一下Trait对象和泛型:

  • Trait对象可以被看作一种数据类型,它总是以引用的方式被使用,在运行期间,它在栈中保存了具体类型的实例数据和实现自该Trait的方法。
  • 泛型不是一种数据类型,它可被看作是数据类型的参数形式或抽象形式,在编译期间会被替换为具体的数据类型

Trait Objecct方式也称为动态分派(dynamic dispatch),它在程序运行期间动态地决定具体类型。而Rust泛型是静态分派,它在编译期间会代码膨胀,将泛型参数转变为使用到的每种具体类型。

例如,类型Square和类型Rectangle都实现了Trait Area以及方法get_area,现在要创建一个vec,这个vec中包含了任意能够调用get_area方法的类型实例。这种需求建议采用Trait Object方式:

fn main(){
  let mut sharps: Vec<&dyn Area> = vec![];
  sharps.push(&Square(3.0));
  sharps.push(&Rectangle(3.0, 2.0));
  println!("{}", sharps[0].get_area());
  println!("{}", sharps[1].get_area());
}

trait Area{
  fn get_area(&self)->f64;
}

struct Square(f64);
struct Rectangle(f64, f64);
impl Area for Square{
  fn get_area(&self) -> f64 {self.0 * self.0}
}
impl Area for Rectangle{
  fn get_area(&self) -> f64 {self.0 * self.1}
}

在上面的示例中,Vec sharps用于保存多种不同类型的数据,只要能调用get_area方法的数据都能存放在此,而调用get_area方法的能力,来自于Area Trait。因此,使用动态的类型dyn Area来描述所有这类数据。当sharps中任意一个数据要调用get_area方法时,都会从它的vtable中查找该方法,然后调用。

但如果改一下上面示例的需求,不仅要为f64实现上述功能,还要为i32、f32、u8等类型实现上述功能,这时候使用Trait Object就很冗余了,要为每一个数值类型都实现一次。

使用泛型则可以解决这类因数据类型而导致的冗余问题。

fn main(){
  let sharps: Vec<Sharp<_>> = vec![
    Sharp::Square(3.0_f64),
    Sharp::Rectangle(3.0_f64, 2.0_f64),
  ];
  sharps[0].get_area();
}

trait Area<T> {
  fn get_area(&self) -> T;
}

enum Sharp<T>{
  Square(T),
  Rectangle(T, T),
}

impl<T> Area<T> for Sharp<T>
  where T: Mul<Output=T> + Clone + Copy
{
  fn get_area(&self) -> T {
    match *self {
      Sharp::Rectangle(a, b) => return a * b,
      Sharp::Square(a) => return a * a,
    }
  }
}

上面使用了泛型枚举,在这个枚举类型上实现Area Trait,就可以让泛型枚举统一各种类型,使得这些类型的数据都具有get_area方法。