我如何处理 Rust 中的包装器类型不变性?
How do I deal with wrapper type invariance in Rust?
对 &Rc<T>
和 &Box<T>
等包装类型的引用在 T
中是不变的(&Rc<T>
不是 &Rc<U>
,即使 T
是一个 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
,但在许多情况下这是不可能的,因为还需要原始类型。那我该怎么办?
在 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());
}
您在这里看到的与方差和子类型完全无关。
首先,关于 Rust 子类型化的信息最丰富的读物是 Nomicon 的 this chapter。你可以在那里发现 Rust 的子类型关系(即当你可以将一种类型的值传递给一个函数或一个期望 different 类型的变量的变量时)是非常有限的。只有在处理生命周期时才能观察到。
例如,下面的一段代码显示了 &Box<T>
究竟是 (co)variant:
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
在它们各自的类型和生命周期参数上都是协变的。
与大多数 "conventional" OOP 语言不同,它们具有 classes/interfaces,如 C++ 和 Java,在 Rust 特征中 不引入子类型关系 。尽管,比如说,
trait Show {
fn show(&self) -> String;
}
非常相似
interface Show {
String show();
}
在某些语言中,如Java,它们在语义上是完全不同的。在 Rust bare trait 中,当用作类型时,never 是实现此特征的任何类型的超类型:
impl Show for i32 { ... }
// the above does not mean that i32 <: Show
Show
,虽然是一个trait,确实可以用在type位置,但是它表示一个特殊的unsized type 只能用来组成 trait objects。您不能拥有裸特征类型的值,因此谈论带有裸特征类型的子类型和方差甚至没有意义。
特征对象采用 &SomeTrait
或 &mut SomeTrait
或 SmartPointer<SomeTrait>
的形式,它们 可以 传递并存储在变量中,它们需要抽象出特征的实际实现。但是,&T
其中 T: SomeTrait
不是 &SomeTrait
的子类型,并且这些类型根本不参与方差。
Trait 对象和常规指针具有不兼容的内部结构:&T
只是一个指向具体类型 T
的常规指针,而 &SomeTrait
是一个包含指向的指针的胖指针实现 SomeTrait
的类型的原始值以及用于实现上述类型的 SomeTrait
的第二个 vtable 指针。
将 &T
作为 &SomeTrait
或 Rc<T>
作为 Rc<SomeTrait>
传递的事实发生是因为 Rust 会 自动强制转换 引用和智能指针:如果它知道 T
,它能够为常规引用 &T
构造一个胖指针 &SomeTrait
;我相信这是很自然的。例如,您使用 Rc::downgrade()
的示例有效,因为 Rc::downgrade()
returns 类型 Weak<MyStruct>
的值被强制转换为 Weak<MyTrait>
.
然而,如果 T: SomeTrait
从 &Box<T>
构造 &Box<SomeTrait>
就复杂得多:一方面,编译器需要分配一个 new 临时值,因为 Box<T>
和 Box<SomeTrait>
具有不同的内存表示。如果你有,比如说,Box<Box<T>>
,从中得到 Box<Box<SomeTrait>>
就更复杂了,因为它需要 在堆上创建一个新分配 来存储 Box<SomeTrait>
。因此,嵌套引用和智能指针没有自动强制转换,而且,这与子类型化和变体完全无关。
对 &Rc<T>
和 &Box<T>
等包装类型的引用在 T
中是不变的(&Rc<T>
不是 &Rc<U>
,即使 T
是一个 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
,但在许多情况下这是不可能的,因为还需要原始类型。那我该怎么办?
在 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());
}
您在这里看到的与方差和子类型完全无关。
首先,关于 Rust 子类型化的信息最丰富的读物是 Nomicon 的 this chapter。你可以在那里发现 Rust 的子类型关系(即当你可以将一种类型的值传递给一个函数或一个期望 different 类型的变量的变量时)是非常有限的。只有在处理生命周期时才能观察到。
例如,下面的一段代码显示了 &Box<T>
究竟是 (co)variant:
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
在它们各自的类型和生命周期参数上都是协变的。
与大多数 "conventional" OOP 语言不同,它们具有 classes/interfaces,如 C++ 和 Java,在 Rust 特征中 不引入子类型关系 。尽管,比如说,
trait Show {
fn show(&self) -> String;
}
非常相似
interface Show {
String show();
}
在某些语言中,如Java,它们在语义上是完全不同的。在 Rust bare trait 中,当用作类型时,never 是实现此特征的任何类型的超类型:
impl Show for i32 { ... }
// the above does not mean that i32 <: Show
Show
,虽然是一个trait,确实可以用在type位置,但是它表示一个特殊的unsized type 只能用来组成 trait objects。您不能拥有裸特征类型的值,因此谈论带有裸特征类型的子类型和方差甚至没有意义。
特征对象采用 &SomeTrait
或 &mut SomeTrait
或 SmartPointer<SomeTrait>
的形式,它们 可以 传递并存储在变量中,它们需要抽象出特征的实际实现。但是,&T
其中 T: SomeTrait
不是 &SomeTrait
的子类型,并且这些类型根本不参与方差。
Trait 对象和常规指针具有不兼容的内部结构:&T
只是一个指向具体类型 T
的常规指针,而 &SomeTrait
是一个包含指向的指针的胖指针实现 SomeTrait
的类型的原始值以及用于实现上述类型的 SomeTrait
的第二个 vtable 指针。
将 &T
作为 &SomeTrait
或 Rc<T>
作为 Rc<SomeTrait>
传递的事实发生是因为 Rust 会 自动强制转换 引用和智能指针:如果它知道 T
,它能够为常规引用 &T
构造一个胖指针 &SomeTrait
;我相信这是很自然的。例如,您使用 Rc::downgrade()
的示例有效,因为 Rc::downgrade()
returns 类型 Weak<MyStruct>
的值被强制转换为 Weak<MyTrait>
.
然而,如果 T: SomeTrait
从 &Box<T>
构造 &Box<SomeTrait>
就复杂得多:一方面,编译器需要分配一个 new 临时值,因为 Box<T>
和 Box<SomeTrait>
具有不同的内存表示。如果你有,比如说,Box<Box<T>>
,从中得到 Box<Box<SomeTrait>>
就更复杂了,因为它需要 在堆上创建一个新分配 来存储 Box<SomeTrait>
。因此,嵌套引用和智能指针没有自动强制转换,而且,这与子类型化和变体完全无关。