如何在Rust中处理包装器类型不变性?

时间:2016-06-01 23:50:10

标签: rust wrapper invariance

&Rc<T>&Box<T>等包装类型的引用在T中不变(&Rc<T>不是&Rc<U>,即使TU use std::rc::Rc; use std::rc::Weak; trait MyTrait {} struct MyStruct { } impl MyTrait for MyStruct {} fn foo(rc_trait: Weak<MyTrait>) {} fn main() { let a = Rc::new(MyStruct {}); foo(Rc::downgrade(&a)); } )。问题的一个具体例子(Rust Playground):

<anon>:15:23: 15:25 error: mismatched types:
 expected `&alloc::rc::Rc<MyTrait>`,
    found `&alloc::rc::Rc<MyStruct>`

此代码导致以下错误:

Box<T>

trait MyTrait {} struct MyStruct { } impl MyTrait for MyStruct {} fn foo(rc_trait: &Box<MyTrait>) {} fn main() { let a = Box::new(MyStruct {}); foo(&a); } Rust Playground)相似的示例(有类似错误):

a

在这些情况下,我当然可以使用所需类型注释Rc::downgrade,但在许多情况下,这是不可能的,因为还需要原始类型。那我该怎么办呢?

2 个答案:

答案 0 :(得分:4)

你在这里看到的与方差和子类型无关。

首先,关于Rust中子类型的最丰富的读物是Nomicon的this chapter。您可以在Rust子类型关系中找到(即,当您可以将一种类型的值传递给一个函数或一个期望不同类型的变量的变量时)非常有限。只有在你终身工作时才能观察到它。

例如,以下代码显示了&Box<T>究竟是如何(共)变体:

fn test<'a>(x: &'a Box<&'a i32>) {}

fn main() {
    static X: i32 = 12;
    let xr: &'static i32 = &X;
    let xb: Box<&'static i32> = Box::new(xr);  // <---- start of box lifetime
    let xbr: &Box<&'static i32> = &xb;
    test(xbr);  // Covariance in action: since 'static is longer than or the 
                // same as any 'a, &Box<&'static i32> can be passed to
                // a function which expects &'a Box<&'a i32>
                //
                // Note that it is important that both "inner" and "outer"
                // references in the function signature are defined with
                // the same lifetime parameter, and thus in `test(xbr)` call
                // 'a gets instantiated with the lifetime associated with
                // the scope I've marked with <----, but nevertheless we are
                // able to pass &'static i32 as &'a i32 because the
                // aforementioned scope is less than 'static, therefore any
                // shared reference type with 'static lifetime is a subtype of
                // a reference type with the lifetime of that scope
}  // <---- end of box lifetime

此程序编译,这意味着&Box对其各自的类型和生命周期参数都是协变的。

与大多数具有C ++和Java等类/接口的“常规”OOP语言不同,在Rust traits 中不引入子类型关系。即便如此,

trait Show {
    fn show(&self) -> String;
}

高度相似

interface Show {
    String show();
}

在像Java这样的语言中,它们在语义上有很大的不同。在Rust裸露特征中,当用作类型时,永远是实现此特征的任何类型的超类型:

impl Show for i32 { ... }

// the above does not mean that i32 <: Show

Show,虽然是一个特征,但可以在类型位置使用,但它表示一个特殊的 unsized type ,它只能用于表单trait objects。你不能拥有裸特征类型的值,因此谈论与裸特征类型的子类型和方差甚至没有意义。

Trait对象采用&SomeTrait&mut SomeTraitSmartPointer<SomeTrait>的形式,可以传递并存储在变量中,需要将它们抽象出来特质的实际实施。但是,&T其中T: SomeTrait 不是 &SomeTrait的子类型,而且这些类型根本不参与差异。

Trait对象和常规指针具有不兼容的内部结构:&T只是指向具体类型T的常规指针,而&SomeTrait是一个胖指针,其中包含指向原始的指针实现SomeTrait的类型的值,以及指向vtable的第二个指针,用于实现上述类型的SomeTrait

&T作为&SomeTraitRc<T>作为Rc<SomeTrait>传递的事实发生了,因为Rust为引用和智能指针执行自动强制:如果知道&SomeTrait,它可以为常规引用&T构造一个胖指针T;我相信这很自然。例如,Rc::downgrade()的示例有效,因为Rc::downgrade()会返回Weak<MyStruct>类型的值,该值会强制转换为Weak<MyTrait>

但是,如果&Box<SomeTrait>构建&Box<T>,则T: SomeTrait要复杂得多:例如,编译器需要分配 new 临时值,因为Box<T>Box<SomeTrait>具有不同的内存表示形式。如果您有Box<Box<T>>,那么从中获取Box<Box<SomeTrait>>会更复杂,因为它需要在堆上创建新的分配来存储Box<SomeTrait> }。因此,嵌套引用和智能指针没有自动强制,再次,这与子类型和方差无关。

答案 1 :(得分:3)

fn foo(rc_trait: Weak<MyTrait>) {} fn main() { let a = Rc::new(MyStruct {}); let b = Rc::downgrade(&a); foo(b); } 的情况下,这实际上只是在这种特殊情况下类型推断的失败,并且如果它作为单独的let完成,它将起作用:

Box<T>

Playground

对于fn foo(rc_trait: &MyTrait) {} fn main() { let a = Box::new(MyStruct {}); foo(a.as_ref()); } ,您很可能实际上不希望引用该框作为参数,而是对内容的引用。在这种情况下,没有不变性来处理:

Rc<T>

Playground

类似地,对于Rc<T>的情况,如果你编写一个带fn foo(rc_trait: Rc<MyTrait>) {} fn main() { let a = Rc::new(MyStruct {}); foo(a.clone()); } 的函数你可能想要一个克隆(即引用计数引用),而不是正常引用:

curl

Playground