Rust中的泛型,trait以及trait objects小记

written on Tue 08 October 2019 by

泛型的使用

泛型的英文为Generic Types,顾名思义就是通用类型。泛型可以帮助我们减少样板代码的编写。设想一下,如果需要为在一个元素为i32的数组中找出最大的那个数,大部分人都会写一个将这部分代码写成一个函数。形如

fn largest(list: &[i32]) -> i32 {
  let mut largest = list[0];

  for &item in list.iter() {
    if item > largest {
      largest = item;
    }
  }

  largest
}

嗯~~ 正常运行。不过之后产品经理增加需求,需要同时支持i64数组,然后又想支持f64数组。在没有泛型的语言中,开发人员只能选择将这个函数复制粘贴n次,对参数类型稍加修改,最后改成不同的名字,如largest_i32largest_i64等。而且,这种情况下,当你需要改变代码时,也需要重复修改多次,这些都或多或少地增加了代码的维护成本。但是泛型的出现,可以减少开发者很多重复的劳动。Rust中的泛型:

fn largest<T>(list: &[T]) -> T {
  let mut largest = list[0];

  for &item in list.iter() {
    if item > largest {
      largest = item;
    }
  }

  largest
}

乍一看,与第一个例子的代码基本相同,但是我们在函数签名中声明了一个泛型T,用来表示任何类型。这样一来,就无需为每一个具体类型定义一个专用函数了。不过不用高兴太早,上面的代码,Rust的编译器并不让通过。这是因为在函数中涉及了对泛型T的大小比较,但是并不是所有的类型都是支持大小比较的,所以还需要再做一点细微的工作来支持它。有兴趣的同学可以去试着让它通过编译。

在本小节的末尾,来说一下Rust中的泛型的工作原理。以上文提到的函数largest为例,编译器会在编译时,找出所有使用了它的具体类型,并为它们分别生成一个largest_xxx函数,这个过程叫做monomorphization,我翻译成特例化。看到这里,有木有很惊讶?Rust做了我们本来要做的脏活累活,它针对特定类型生成特定函数,这样的泛型实现方式是没有动态开销的,只有编译开销: )

Trait的使用

Rust中的Trait有点像其他语言中的Interface,但是还是有机制上的区别。Trait定义了一组共享方法,是一种抽象方式。比如:

trait Human {
  fn talk(&self) -> String;
}

fn listen_to(person: impl Human) {
    let sent = person.talk();
  println!("man say: {}", sent);
}

我们定义了一个Human的trait,其中还有一个talk方法。接着,我们还定义了一个函数,参数类型为实现了trait的类型。trait不能直接作为函数的参数,但是可以作为trait bound使用,简单理解就是对具体类型的限定。比如,在上面的例子中,我们就限定了man的类型必须实现trait Human。

形如impl trait的写法是一种简便写法,通常写法是和泛型结合。

fn listen_to<T: Human>(person: T) {...}
//或者

fn listen_to<T>(person:T) where T: Human {...}

一个泛型可以添加多个trait bound,使用+串联。比如T: Human + Display

规定了trait限定之后,我们需要给函数传入具体的类型。该类型必须实现相应的trait。

struct Man;

impl Human for Man {
  fn talk(&self) -> String {
        "Man: I am a man".into()
  }
}

fn main () {
  let man = Man;
  listen_to(man);
}

很简单,理解上也没有问题。不过,接下来就会说到一个问题。我们来看个例子

struct Woman;

impl Human for Woman {
  fn talk(&self) -> String {
        "Woman: I am a woman".into()
  }
}

fn listen_to<T: Human>(man: T, woman: T) {
    println!("{}", man.talk());
    println!("{}", woman.talk());
}

fn main() {
  let man = Man;
  let woman = Woman;
  listen_to(man, woman);
}

什么!编译器报错了.

   |
29 |   listen_to(man, woman);
   |                  ^^^^^ expected struct `Man`, found struct `Woman`
   |
   = note: expected type `Man`
              found type `Woman`

如果经常写Go的同学可能会奇怪了,listen_to函数的man和woman两个参数都是Human这个interface类型,我们实际传入的也是两个实现了Human的类型,为什么会报错呢?哈哈,这又是Rust的泛型机制在作怪了,我们对man和woman都使用了同一泛型T,这需要我们保证实际传入的类型必须为同一类型。

那么如何修改呢?有两个方法

// 方法一,使用两个泛型
fn listen_to<M: Human, W: Human>(man: M, woman: W) {...}

// 方法二,使用关键词impl
fn listen_to(man: impl Human, W: impl Human) {...}

目前看来,好像一切都相安无事了。但是由于Rust泛型机制的限制,貌似失去了动态能力。想象一下,如果我们需要一个包含了多个Human的Vec,里面既有Man,又有Woman,我们该怎么办?有的同学说简单,直接用不就好了吗?

let people: Vec<Human> = vec![];

不好意思,编译器不通过!如果要做到我们想要的效果,需要使用trait objects,编译器告诉你要使用dyn关键词。

let people: Vec<dyn Human> = vec![];

再次编译,吼吼,还是不通过!因为编译阶段无法知道trait objects的实际大小了,所以呢?需要我们使用Box将它放到堆上去。

let people: Vec<Box<dyn Human>> = vec![];

终于过了,不容易吼。

fn listen_to_people(people: Vec<Box<dyn Human>>) {
  for one in people {
    println!("{}", one.talk());
  }
}

fn main() {
  let mut people: Vec<Box<dyn Human>> = vec![];
  people.push(Box::new(Man));
  people.push(Box::new(Woman));
  people.push(Box::new(Man));
  listen_to_people(people);
}

trait objects涉及动态匹配,会有运行时开销。

补充一下,只有类型安全且不含泛型参数的trait才可以作为trait objects.

  • 返回类型不能为Self
  • 不能还有泛型参数

比如,Clone这个trait就不能作为trait objects,因为方法的返回类型为Self

pub trait Clone {
    fn clone(&self) -> Self;
}

这一篇,篇幅较短。就是简要记录一下泛型、Trait以及Trait objects的使用。如有错误,敬请指教。

This entry was tagged on #rust

comments powered by Disqus
 

Tags