What you see here is not related to variance and subtyping at all.
First, the most informative read on subtyping in Rust is this chapter of Nomicon. You can find there that in Rust subtyping relationship (i.e. when you can pass a value of one type to a function or a variable which expects a variable of different type) is very limited. It can only be observed when you're working with lifetimes.
For example, the following piece of code shows how exactly &Box<T>
is (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
This program compiles, which means that both &
and Box
are covariant over their respective type and lifetime parameters.
Unlike most of "conventional" OOP languages which have classes/interfaces like C++ and Java, in Rust traits do not introduce subtyping relationship. Even though, say,
trait Show {
fn show(&self) -> String;
}
highly resembles
interface Show {
String show();
}
in some language like Java, they are quite different in semantics. In Rust bare trait, when used as a type, is never a supertype of any type which implements this trait:
impl Show for i32 { ... }
// the above does not mean that i32 <: Show
Show
, while being a trait, indeed can be used in type position, but it denotes a special unsized type which can only be used to form trait objects. You cannot have values of the bare trait type, therefore it does not even make sense to talk about subtyping and variance with bare trait types.
Trait objects take form of &SomeTrait
or &mut SomeTrait
or SmartPointer<SomeTrait>
, and they can be passed around and stored in variables and they are needed to abstract away the actual implementation of the trait. However, &T
where T: SomeTrait
is not a subtype of &SomeTrait
, and these types do not participate in variance at all.
Trait objects and regular pointers have incompatible internal structure: &T
is just a regular pointer to a concrete type T
, while &SomeTrait
is a fat pointer which contains a pointer to the original value of a type which implements SomeTrait
and also a second pointer to a vtable for the implementation of SomeTrait
of the aforementioned type.
The fact that passing &T
as &SomeTrait
or Rc<T>
as Rc<SomeTrait>
works happens because Rust does automatic coercion for references and smart pointers: it is able to construct a fat pointer &SomeTrait
for a regular reference &T
if it knows T
; this is quite natural, I believe. For instance, your example with Rc::downgrade()
works because Rc::downgrade()
returns a value of type Weak<MyStruct>
which gets coerced to Weak<MyTrait>
.
However, constructing &Box<SomeTrait>
out of &Box<T>
if T: SomeTrait
is much more complex: for one, the compiler would need to allocate a new temporary value because Box<T>
and Box<SomeTrait>
has different memory representations. If you have, say, Box<Box<T>>
, getting Box<Box<SomeTrait>>
out of it is even more complex, because it would need creating a new allocation on the heap to store Box<SomeTrait>
. Thus, there are no automatic coercions for nested references and smart pointers, and again, this is not connected with subtyping and variance at all.
&'a T
is a subtype of&'static T
for any'a
). You can find more here. – Barabarabarabas&T
is variant overT
andBox<T>
is also variant overT
. Yet, as my question shows,&Box<T>
is not variant overT
. If you can explain why that is, please do share it as an answer (if that insight also gives some alternate way of dealing with the invariance, all the better). – Brainwashing&Box<T>
is not variant overT
, because the only subtyping relationship in Rust is tied to lifetimes (as in&'a T <: &'b T
if'a
is larger than'b
), but your question contains only trait implementations, which, as I said before, are not related to subtyping and therefore variance in any way. Try passing, say,&Box<&'static i32>
to a function which expects&Box<&i32>
and you will see the covariance in action: gist.github.com/20983300fcf6872a583db38cfe96d663 – Barabarabarabas