Covariance of Box type in Rust

2019-07-20 19:24发布

问题:

After I read the subtyping chapter of the Nomicon, I couldn't wrap my head around covariance of a type parameter. Especially for the Box<T> type, which is described as: T is covariant.

However, if I write this code:

trait A {}
trait B: A {}

struct C;
impl A for C {}
impl B for C {}

fn foo(v: Box<A>) {}

fn main() {
    let c = C;
    let b: Box<B> = Box::new(c);
    foo(b);
}

(Playground)

error[E0308]: mismatched types
  --> src/main.rs:13:9
   |
13 |     foo(b);
   |         ^ expected trait `A`, found trait `B`
   |
   = note: expected type `std::boxed::Box<(dyn A + 'static)>`
              found type `std::boxed::Box<dyn B>`

B is clearly a "subtype" of A and Box is covariant over its input. I don't know why it doesn't work or why it won't do any type coercion. Why would they consider Box<T> to be covariant where the only use cases are invariants?

回答1:

What subtyping and variance means in Rust

The Nomicon is not a fully polished document. Right now, 5 of the most recent 10 issues in that repo specifically deal with subtyping or variance based on their title alone. The concepts in the Nomicon can require substantial effort, but the information is generally there.

First off, check out some initial paragraphs (emphasis mine):

Subtyping in Rust is a bit different from subtyping in other languages. This makes it harder to give simple examples, which is a problem since subtyping, and especially variance, are already hard to understand properly.

To keep things simple, this section will consider a small extension to the Rust language that adds a new and simpler subtyping relationship. After establishing concepts and issues under this simpler system, we will then relate it back to how subtyping actually occurs in Rust.

It then goes on to show some trait-based code. Reiterating the point, this code is not Rust code anymore; traits do not form subtypes in Rust!

Later on, there's this quote:

First and foremost, subtyping references based on their lifetimes is the entire point of subtyping in Rust. The only reason we have subtyping is so we can pass long-lived things where short-lived things are expected.

Rust's notion of subtyping only applies to lifetimes.

What's an example of subtyping and variance?

Variant lifetimes

Here's an example of subtyping and variance of lifetimes at work inside of a Box.

A failing case

fn smaller<'a>(v: Box<&'a i32>) {
    bigger(v)
}

fn bigger(v: Box<&'static i32>) {}
error[E0308]: mismatched types
 --> src/lib.rs:2:12
  |
2 |     bigger(v)
  |            ^ lifetime mismatch
  |
  = note: expected type `std::boxed::Box<&'static i32>`
             found type `std::boxed::Box<&'a i32>`
note: the lifetime 'a as defined on the function body at 1:12...
 --> src/lib.rs:1:12
  |
1 | fn smaller<'a>(v: Box<&'a i32>) {
  |            ^^
  = note: ...does not necessarily outlive the static lifetime

A working case

fn smaller<'a>(v: Box<&'a i32>) {}

fn bigger(v: Box<&'static i32>) {
    smaller(v)
}

Invariant lifetimes

Here's a case that works:

struct S<'a>(&'a i32);

fn smaller<'a>(_v: &S<'a>, _x: &'a i32) {}

fn bigger(v: &S<'static>) {
    let x: i32 = 1;
    smaller(v, &x);
}

The same code with all the references changed to mutable references will fail because mutable references are invariant:

struct S<'a>(&'a mut i32);

fn smaller<'a>(_v: &mut S<'a>, _x: &'a mut i32) {}

fn bigger(v: &mut S<'static>) {
    let mut x: i32 = 1;
    smaller(v, &mut x);
}
error[E0597]: `x` does not live long enough
 --> src/lib.rs:7:16
  |
7 |     smaller(v, &mut x);
  |     -----------^^^^^^-
  |     |          |
  |     |          borrowed value does not live long enough
  |     argument requires that `x` is borrowed for `'static`
8 | }
  | - `x` dropped here while still borrowed

Addressing specific points

B is clearly a "subtype" of A

It is not.

Box is covariant over its input

It is, where covariance is only applicable to lifetimes.

I don't know why it doesn't work or why it won't do any type coercion.

This is covered by Why doesn't Rust support trait object upcasting?

Why would they consider Box<T> to be covariant

Because it is, for the things in Rust to which variance is applied.

See also

  • How do I deal with wrapper type invariance in Rust?
  • Why does linking lifetimes matter only with mutable references?
  • What is an example of contravariant use in Rust?