如何处理Rust中的包装类型不变性



对包装器类型(如&Rc<T>&Box<T>)的引用在T中是不变的(即使TU&Rc<T>也不是&Rc<U>)。这个问题的一个具体例子(Rust Playground):

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));
}

此代码导致以下错误:

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

Box<T>(Rust Playground)的类似示例(有类似错误):

trait MyTrait {}
struct MyStruct {
}
impl MyTrait for MyStruct {}
fn foo(rc_trait: &Box<MyTrait>) {}
fn main() {
let a = Box::new(MyStruct {});
foo(&a);
}

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

您在这里看到的与方差和分型根本无关。

首先,关于Rust中的子类型,信息量最大的是Nomicon的这一章。你可以在那里发现,在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中的特征没有引入子类型关系。即使

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

高度类似

interface Show {
String show();
}

在像Java这样的一些语言中,它们在语义上有很大的不同。在Rust中,当用作类型时,bare trait是never实现该trait的任何类型的超类型:

impl Show for i32 { ... }
// the above does not mean that i32 <: Show

Show虽然是一个特征,但确实可以用于类型位置,但它表示一个特殊的无大小类型,只能用于形成特征对象。你不可能有裸特征类型的值,所以谈论裸特征类型中的亚型和变异是没有意义的。

特性对象采用&SomeTrait&mut SomeTraitSmartPointer<SomeTrait>的形式,它们可以传递并存储在变量中,需要它们来抽象特性的实际实现。然而,&T,其中T: SomeTrait不是&SomeTrait的亚型,并且这些类型根本不参与方差。

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

&T作为&SomeTrait传递或将Rc<T>作为Rc<SomeTrait>传递之所以有效,是因为Rust对引用和智能指针执行自动强制:如果它知道T,则能够为常规引用&T构造胖指针&SomeTrait;我相信这很自然。例如,您使用Rc::downgrade()的示例之所以有效,是因为Rc::downgrade()返回类型为Weak<MyStruct>的值,该值被强制为Weak<MyTrait>

然而,如果T: SomeTrait,则从&Box<T>构造&Box<SomeTrait>要复杂得多:例如,编译器需要分配一个新的临时值,因为Box<T>Box<SomeTrait>具有不同的内存表示。如果您有Box<Box<T>>,那么从中获取Box<Box<SomeTrait>>就更加复杂了,因为它需要在堆上创建一个新的分配来存储Box<SomeTrait>。因此,嵌套引用和智能指针没有自动强制,而且,这与子类型和方差根本没有关系。

Rc::downgrade的情况下,这实际上只是该特定情况下类型推理的失败,如果作为单独的let进行,则会起作用:

fn foo(rc_trait: Weak<MyTrait>) {}
fn main() {
let a = Rc::new(MyStruct {});
let b = Rc::downgrade(&a);
foo(b);
}

游乐场

对于Box<T>,很可能您实际上并不希望引用框作为参数,而是希望引用内容。在这种情况下,没有不变性可处理:

fn foo(rc_trait: &MyTrait) {}
fn main() {
let a = Box::new(MyStruct {});
foo(a.as_ref());
}

游乐场

类似地,对于Rc<T>的情况,如果您编写一个使用Rc<T>的函数,您可能想要一个克隆(即引用计数的引用),而不是一个正常引用:

fn foo(rc_trait: Rc<MyTrait>) {}
fn main() {
let a = Rc::new(MyStruct {});
foo(a.clone());
}

游乐场

相关内容

  • 没有找到相关文章

最新更新