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
    }