【译】Rust中的Sizedness (四)

413 阅读7分钟

原文链接: https://github.com/pretzelhammer/rust-blog/blob/master/posts/sizedness-in-rust.md

原文标题: Sizedness in Rust

公众号: Rust碎碎念

Trait对象的限制(Trait Object Limitations)

即使一个trait是对象安全的,仍然存在sizeness相关的边界情况,这些情况限制了什么类型可以转成trait对象以及多少种trait和什么样的trait可以通过一个trait对象来表示。

不能把不确定大小类型(unsized type)转成trait对象

fn generic<TToString>(t: T{}
fn trait_object(t: &dyn ToString{}

fn main({
    generic(String::from("String")); // compiles
    generic("str"); // compiles
    trait_object(&String::from("String")); // compiles, unsized coercion
    trait_object("str"); // compile error, unsized coercion impossible
}

抛出下面的错误:

error[E0277]: the size for values of type `str` cannot be known at compilation time
 --> src/main.rs:8:18
  |
8 |     trait_object("str"); // compile error
  |                  ^^^^^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `str`
  = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
  = note: required for the cast to the object type `dyn std::string::ToString`

传递一个&String到一个期望接收参数类型是&dyn ToString的函数能够工作是因为类型强制转换(type coercion)。String实现了ToString(这个trait)并且我们通过一个不确定大小强制转换(unsized coercion)可以把像String这样的确定大小类型(sized type)转换成一个像dyn ToString这样的不确定大小类型(unsized type)。str也实现了ToString,并且把str转换成一个dyn ToString也需要一个不确定大小强制转换(unsized coercion),但是str已经是不确定大小(unsized)的了!我们怎么样能把一个已经是不确定大小类型转成另一个不确定大小类型?

&str指针是双宽度(double-width)的,存储了一个指向数据的指针和数据长度。&dyn ToString指针也是双宽度(double-width),存储了一个指向数据的指针和一个指向vtable的指针。要把一个&str强制转换成&dyn toString将会需要一个三倍宽度(triple-width)的指针来存储执行数据的指针、数据的长度、指向vtable的指针。Rust不支持三倍宽度(triple-width)的指针,所以把一个不确定大小类型转为一个trait对象是不可能的。

上面的两段可以用下面这张表来总结:

类型(Type) 指向数据的指针(Pointer to Data) 数据长度(Data Length) 指向VTable的指针(Pointer to VTable) 总长度(Total Width)
&String ✔️ 1 ✔️
&str ✔️ ✔️ 2 ✔️
&String as &dyn ToString ✔️ ✔️ 2 ✔️
&str as &dyn ToString ✔️ ✔️ ✔️ 3 ❌

不能创建多Trait的对象(Cannot create Multi-Trait Objects)

trait Trait {}
trait Trait2 {}

fn function(t: &(dyn Trait + Trait2){}

抛出:

error[E0225]: only auto traits can be used as additional traits in a trait object
 --> src/lib.rs:4:30
  |
4 | fn function(t: &(dyn Trait + Trait2)) {}
  |                      -----   ^^^^^^
  |                      |       |
  |                      |       additional non-auto trait
  |                      |       trait alias used in trait object type (additional use)
  |                      first non-auto trait
  |                      trait alias used in trait object type (first use)

记住:一个trait对象指针是双宽度的:存储一个指向数据的指针和一个指向vtable的指针。但是,这里有两个trait,所以有两个vtable,这就要求&(dyn Trait + Trait2)的指针是3个宽度的。像SyncSend这样的自动trait(Auto-trait)可以被允许是因为他们没有方法也就没有vtable。

这种情况的解决方法是通过另一个trait把(多个)trait进行组合的方式来把vtable进行拼接,像下面这样:

trait Trait {
    fn method(&self{}
}

trait Trait2 {
    fn method2(&self{}
}

trait Trait3Trait + Trait2 {}

// auto blanket impl Trait3 for any type that also impls Trait & Trait2
impl<T: Trait + Trait2> Trait3 for T {}

// from `dyn Trait + Trait2` to `dyn Trait3` 
fn function(t: &dyn Trait3{
    t.method(); // compiles
    t.method2(); // compiles
}

这种方法的一个缺点在于,Rust不支持supertait向上转换。这意味着,如果我们有一个&dyn Trait3,但是我们不能在需要dyn Traitdyn Trait2的地方使用它。下面的程序无法编译:

trait Trait {
    fn method(&self) {}
}

trait Trait2 {
    fn method2(&self) {}
}

trait Trait3: Trait + Trait2 {}

impl<T: Trait + Trait2> Trait3 for T {}

struct Struct;
impl Trait for Struct {}
impl Trait2 for Struct {}

fn takes_trait(t: &dyn Trait) {}
fn takes_trait2(t: &dyn Trait2) {}

fn main() {
    let t: &dyn Trait3 = &Struct;
    takes_trait(t); // compile error
    takes_trait2(t); // compile error
}

抛出:

error[E0308]: mismatched types
  --> src/main.rs:22:17
   |
22 |     takes_trait(t);
   |                 ^ expected trait `Trait`, found trait `Trait3`
   |
   = note: expected reference `&dyn Trait`
              found reference `&dyn Trait3`

error[E0308]: mismatched types
  --> src/main.rs:23:18
   |
23 |     takes_trait2(t);
   |                  ^ expected trait `Trait2`, found trait `Trait3`
   |
   = note: expected reference `&dyn Trait2`
              found reference `&dyn Trait3`

这是因为dyn Trait3是一个不同于dyn Traitdyn Trait2的类型,因为它们有不同的vtable布局,尽管dyn Trait3的确包含了dyn Traitdyn Trait2的所有方法。这里的解决办法是添加显式的转换方法:

trait Trait {}
trait Trait2 {}

trait Trait3: Trait + Trait2 {
    fn as_trait(&self-> &dyn Trait;
    fn as_trait2(&self-> &dyn Trait2;
}

impl<T: Trait + Trait2> Trait3 for T {
    fn as_trait(&self-> &dyn Trait {
        self
    }
    fn as_trait2(&self-> &dyn Trait2 {
        self
    }
}

struct Struct;
impl Trait for Struct {}
impl Trait2 for Struct {}

fn takes_trait(t: &dyn Trait) {}
fn takes_trait2(t: &dyn Trait2) {}

fn main() {
    let t: &dyn Trait3 = &Struct;
    takes_trait(t.as_trait()); // compiles
    takes_trait2(t.as_trait2()); // compiles
}

这是一种简单且直接的解决办法,似乎可以让编译器来为我们自动化实现。正如我们在解引用和不确定大小强制转换中所见,Rust并不羞于执行强制类型转换,那么,为什么没有一个trait的向上强制转换?这个问题很好,答案也很熟悉:Rust核心团队正在研究其他具有更高优先级和更具影响力的的特性。很好。

关键点(Key Takeaway)

  • Rust不支持超过2个宽度的指针,所以

    • 我们不能够把不确定大小类型(unsized type)转换为trait对象
    • 我们不能有多trait对象,但是我们可以通过把多个trait合并到一个trait里来解决

本文禁止转载,谢谢配合!欢迎关注我的微信公众号: Rust碎碎念

Rust碎碎念
Rust碎碎念