Rust入门失败之Traits&Generics

Trait

  • Trait有scope的概念, 必须声明可见后才能使用. 这样可以避免名称冲突, 同时也可以灵活地自定义实现Trait

  • 类似C/C++的虚基类接口定义, 但是最明显的差别是:

    • C/C++的虚基类继承会要求在子类每个实例的内存模型中生成虚函数表, 占用对象内存.
    • 而为一个struct实现一个Trait对struct实例本身没有任何内存开销.
  • Trait本身并没有固定的大小(只有引用某个实现该Trait类型的实例后才有占用大小一说), 因此不能直接声明和使用Trait类型的变量. 对某个实现该Trait类型的实例的有效引用称为Trait Object

    1
    2
    3
    4
    5
    use std::io::Write;

    let mut buf: Vec<u8> = vec![];
    let writter: Wirte = buf; // error: `Write` does not have a constant size
    let writter: &mut Write = &mut buf; // Ok, writter is a Trait Object
  • Trait Object实际是一个fat pointer, 占用两个机器字字节, 一个指向实际的实例对象, 一个指向虚基表vtable. 注意, 这个vtable本身不属于实例, 而是当作为一个Trait Object进行引用时附加给这个引用的fat pointer, 内存布局如下:

    image-20190831011855635

  • 默认方法(Default method), 可以在trait的声明中定义默认方法.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn write_all(&mut self, buf: &[u8]) -> Result<()> {
    let mut bytes_written = 0;
    while bytes_written < buf.len() {
    bytes_written += self.write(&buf[bytes_written...])?;
    }
    Ok(())
    }
  • 扩展trait(extension trait), 可以为其它trait类型扩展出自定义trait的接口.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    trait WriteHtml {
    fn write_html(&mut self, html: &HtmlDocument) -> io::Result<()>;
    }

    // For every type W that implements Write, here's an implementation for WriteHtml for W
    impl<W: Write> WriteHtml for W {
    fn write_html(&mut self, html: &HtmlDocument) -> io::Result<()> {
    ...
    }
    }
  • Self关键字(not self), 用来作为别名, 声明为实现该Trait的具体类型.

    1
    2
    3
    pub trait Clone {
    fn clone(&self) -> Self;
    }
  • Subtrait, 类似接口继承. 可以这么理解: 凡是实现了Subtrait(Creature)的类型, 也必须实现父Trait(Visible)的所有方法., 有的地方又叫Extend trait.

    1
    2
    3
    4
    // Every type that implements Creature must also implement the Visible trait.
    trait Creature: Visible {
    fn ...;
    }
  • Static Methods, 类似接口的静态方法, 没有self参数, 通常用作工厂模式.

    1
    2
    3
    4
    trait StringSet {
    fn new() -> Self;
    fn from_slice(strings: &[&str]) -> Self;
    }

    调用方式是实现该trait的类型名称加上::, 如果是泛型(下面会介绍), 则用泛型变量.

    1
    2
    3
    4
    5
    6
    7
    // SortedStringSet impl the StringSet trait
    let set1 = SortedStringSet::new();

    fn func<S: StringSet>() -> S {
    let mut set2 = S::new();
    ...
    }

Generic

  • 泛型Generic用<>来表示, 通常用全大写变量表示泛型参数.

    1
    2
    fn hello(out: &mut Write){}
    fn hello<W: Write>(out: &mut W){}
  • 编译时Rust会为使用泛型的每个类型生成一套机器代码, 没有运行时开销, 但是会生成额外的机器码, 增加二进制大小.

  • 可以使用泛型的场景:

    • 函数
    • enum、struct及其method
    • 类型别名(type aliases): type PancakeResult<T> = Result<T, PancakeError>;
    • 函数的where语句

Difference between Trait & Generic

  1. Generic没有运行时开销, 速度更快, 而Trait需要运行时才能知道实际类型(也就是执行时根据Trait Object来查找vtable方法). 如果需要处理大量各种不同类型对象的特性或者对二进制大小有要求, 可以用Trait. 否则建议使用Generic.

  2. Not every trait can support trait objects. 并不是所有的Trait都支持Trait Object方式使用. 包含静态方法的Trait, 由于没有self参数, 在泛型函数中其静态方法不能通过Trait Object来调用. 例如下面(仍然以上面静态方法代码为例):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Return the set of words in `document` that aren't in `wordlist`.
    fn unknown_words<S: StringSet>(document: &Vec<String>, wordlist: &S) -> S {
    // 不能通过Trait Object wordlist 调用new, 只能通过泛型变量S::
    let mut unknowns = S::new();
    for word in document {
    if !wordlist.contains(word) {
    unknowns.add(word);
    }
    }
    unknowns
    }