Trait和Trait Objec

Trait和Trait Object

从多种数据类型中抽取这些类型之间可通用的方法或属性,并将它们放进一个更抽象的类型中,这是一种很好的代码复用方式,也是多态的一种体现方式。
在面向对象中,这种功能一般通过接口(interface)实现。在rust中,通过trait 实现。trait 类似其他语言中的接口概念。例如 trait 可以被其他类型实现(implement),也可以在trait 中定义一些方法,实现该trait 的类型都必须实现这些方法。

严格来说,Rust中Trait的作用主要体现在两方面:

1.Trait类型:用于定义抽象行为,抽取那些共性的属性,主要表现是作为泛型的数据类型(对泛型进行限制)
2.Trait对象:即Trait Object,能用于多态

Trait是对多种类型之间的共性进行的抽象,它只规定实现它的类型要定义哪些方法以及这些方法的签名,至于方法体的逻辑则不关心。

也可以换个角度来看待Trait。Trait描述了一种通用功能,这种通用功能要求具有某些行为,这种通用功能可以被很多种类型实现,每个实现了这种通用功能的类型,都可以被称之为是【具有该功能的类型】。

例如 clone trait 是一种通用类型,描述克隆的类型,i32 类型、i64 类型、Vec类型 都实现了clone trait,那么可以说 i32,i64,Vec 这几个类型都具有clone 的功能,可以调用clone()方法。

一个类型可以实现多个trait,使得这个类型有很多功能,可以调用这些Trait的方法。查看 i32类型的官方文档,会发现i32 类型实现了非常多的trait .i32类型的绝大多数功能都来自于其实现的各种Trait,用术语来说,那就是i32类型的大多数功能是组合(composite)其他各种Trait而来的(组合优于继承的组合)。

因此,Rust是一门支持组合的语言:通过实现Trait而具备相应的功能,是组合而非继承。

derive trait

对于Struct类型、Enum类型,需要自己手动去实现各Trait。

但对于一些常见的Trait,可在Struct类型或Enum类型前使用#[derive()]简单方便地实现这些Trait,Rust会自动为Struct类型和Enum类型定义好这些Trait所要求实现的方法。

例如,为下面的Struct类型、Enum类型实现Copy Trait、Clone Trait。

#[derive(Copy, Clone)]
struct Person {
  name: String,
  age: u8,
}

#[derive(Copy, Clone)]
enum Direction {
  Up,
  Down,
  Left,
  Right,
}

现在,Person类型和Direction 类型就都实现了 Copy Trait 和 Clone Trait,具有这两个 Trait的功能:所有权转移时是可拷贝、可克隆的。

Trait 的作用域

Rust允许在任何时候为任何类型实现任何Trait。

Trait 继承

通过让某个类型去实现某个trait,使得该类型具备该trait的功能,是组合(composite)的方式。
继承通常用来描述属于同种性质的父子关系(is a),而组合用来描述具有某功能(has a)。

例如,支持继承的语言,可以让轿车类型(Car)继承交通工具类型(Vehicle),表明轿车是一种(is a)交通工具,它们是同一种性质的东西。而如果是支持组合的语言,可以定义可驾驶功能Drivable,然后将Driveable组合到轿车类型、轮船类型、飞机类型、卡车类型、玩具车类型,等等,表明这些类型具有(has a)驾驶功能。

Rust除了支持组合,还支持继承。但Rust只支持Trait之间的继承,比如Trait A继承Trait B。实现继承的方式很简单,在定义Trait A时使用冒号加上Trait B即可。

trait B{}
trait A:B{}

如果Trait A 继承 Trait B,当类型C 想要实现Trait A时,同时需要实现B。

trait B{
    fn fnB(&self);
}
// trait A 继承 Trait B
trait A:B{
    fn fnA(&self);
}
struct C{}
//c 实现 trait A
impl A for C{
    fn fnA(&self){
        println!("impl fnA");
    }
}
//c 还需要实现 traitB
 impl B for C{
    fn fnB(&self){
        println!("impl fnB");
    }
}

C的实例对象还需要调用 fnA() 与fnB()

fn main(){
    let c = C{};
    c.fnA();
    c.fnB();
}

理解 trait object

trait 的另一个作用是 Trait Object 。
理解Trait Object也简单:当Car、Boat、Bus实现了Trait Drivable后,在需要Drivable类型的地方,都可以使用实现了Drivable的任意类型,如Car、Boat、Bus。从场景需求来说,需要Drivable的地方,其要求的是具有可驾驶功能,而实现了Drivable的Car、Bus等类型都具有可驾驶功能。

这和鸭子模型(Duck Typing)有点类似,只要叫起来像鸭子,它就可以当成鸭子来使用。也就是说,真正需要的不是鸭子,而是鸭子的叫声。

Rust无法将Trait作为数据类型来用。这很容易理解,因为一种类型可能实现了多种Trait,将其实现的一种trait 作为数据类型,显然无法代替该数据类型。

Rust真正支持的用法是:虽然Trait自身不能当作数据类型来使用,但Trait Object可以当作数据类型来使用。因此,可以将实现了Trait A的类型B、C、D当作Trait A的Trait Object来使用。也就是说,Trait Object是Rust支持的一种数据类型,它可以有自己的实例数据,就像Struct类型有自己的实例对象一样。

可以将Trait Object和Slice做对比,它们在不少方面有相似之处。

1.对于类型T,写法[T]表示类型T的Slice类型,由于Slice的大小不固定,因此几乎总是使用Slice的引用方式&[T],Slice的引用保存在栈中,包含两份数据:Slice所指向数据的起始指针和Slice的长度。

2.对于Trait A,写法dyn A表示Trait A的Trait Object类型,由于Trait Object的大小不固定,因此几乎总是使用Trait Object的引用方式&dyn A,Trait Object的引用保存在栈中,包含两份数据:Trait Object所指向数据的指针和指向一个虚表vtable的指针。

上面所描述的Trait Object,还有几点需要解释:

  • Trait Object大小不固定:这是因为,对于Trait A,类型B可以实现Trait A,类型C也可以实现Trait A,因此Trait Object没有固定大小
    2.几乎总是使用Trait Object的引用方式:

  • 虽然Trait Object没有固定大小,但它的引用类型的大小是固定的,它由两个指针组成,因此占用两个指针大小,即两个机器字长

  • 一个指针指向实现了Trait A的具体类型的实例,也就是当作Trait A来用的类型的实例,比如B类型的实例、C类型的实例等

  • 另一个指针指向一个虚表vtable,vtable中保存了B或C类型的实例对于可以调用的实现于A的方法。
    当调用方法时,直接从vtable中找到方法并调用。之所以要使用一个vtable来保存各实例的方法,是因为实现了Trait A的类型有多种,这些类型拥有的方法各不相同,当将这些类型的实例都当作Trait A来使用时(此时,它们全都看作是Trait A类型的实例),有必要区分这些实例各自有哪些方法可调用

  • Trait Object的引用方式有多种。例如对于Trait A,其Trait Object类型的引用可以是&dyn A、Box、Rc等

简而言之,当类型B实现了Trait A时,类型B的实例对象b可以当作A的Trait Object类型来使用,b中保存了作为Trait Object对象的数据指针(指向B类型的实例数据)和行为指针(指向vtable)

一定要注意,此时的b被当作A的Trait Object的实例数据,而不再是B的实例对象,而且,b的vtable只包含了实现自Trait A的那些方法,因此b只能调用实现于Trait A的方法,而不能调用类型B本身实现的方法和B实现于其他Trait的方法。也就是说,当作哪个Trait Object来用,它的vtable中就包含哪个Trait的方法。

比如 v 是包含i32 类型数据的Vec,v的 类型是Vec 而非i32,但v中保存了 i32类型的实例数据,另外 v也只可以调用Vec的方法而不可以调用 i32的方法。

trait A{
  fn a(&self){println!("from A");}
}

trait X{
  fn x(&self){println!("from X");}
}

// 类型B同时实现trait A和trait X
// 类型B还定义自己的方法b
struct B{}
impl B {fn b(&self){println!("from B");}}
impl A for B{}
impl X for B{}

fn main(){
  // bb是A的Trait Object实例,
  // bb保存了指向类型B实例数据的指针和指向vtable的指针
  let bb: &dyn A = &B{};
  bb.a();  // 正确,bb可调用实现自Trait A的方法a()
  bb.x();  // 错误,bb不可调用实现自Trait X的方法x()
  bb.b();  // 错误,bb不可调用自身实现的方法b()
}

使用 Trait Object类型

了解Trait Object之后,使用它就不再难了,它也只是一种数据类型罢了。
例如:

rait Playable {
  fn play(&self);
  fn pause(&self) {println!("pause");}
  fn get_duration(&self) -> f32;
}

// Audio类型,实现Trait Playable
struct Audio {name: String, duration: f32}
impl Playable for Audio {
  fn play(&self) {println!("listening audio: {}", self.name);}
  fn get_duration(&self) -> f32 {self.duration}
}

// Video类型,实现Trait Playable
struct Video {name: String, duration: f32}
impl Playable for Video {
  fn play(&self) {println!("watching video: {}", self.name);}
  fn pause(&self) {println!("video paused");}
  fn get_duration(&self) -> f32 {self.duration}
}

现在,将 Audio 的实例或 Vedio的实例当做 Playable 的 Trait Object 来使用:

fn main() {
  let x: &dyn Playable = &Audio{
    name: "telephone.mp3".to_string(),
    duration: 3.42,
  };
  x.play();
  
  let y: &dyn Playable = &Video{
    name: "Yui Hatano.mp4".to_string(),
    duration: 59.59,
  };
  y.play();
}

此时,x的数据类型是Playable的Trait Object类型的引用,它在栈中保存了一个指向Audio实例数据的指针,还保存了一个指向包含了它可调用方法的vtable的指针。同理,y也一样。

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方法。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值