对包装器类型(如&Rc<T>
和&Box<T>
)的引用在T
中是不变的(即使T
是U
,&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 SomeTrait
或SmartPointer<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());
}
游乐场