Variance in Rust (Rust 中的协变、逆变与不变)
I’m watching Jon Gjengset’s live coding stream. And the topic is “Subtyping and Variance”. This is my note.
Jon Gjengset’s live coding stream
Lifetime shrinking
The Rust compiler will automatically shrink the lifetime of the parameter to the shortest one. For example,
fn main() {
let s = String::new();
let x = "static str"; // `x` is `&'static str`
let mut y = &*s; // `y` is `&'s str`
y = x;
// Still compilable!
// Rust automatically shrink the lifetime static to s.
}
That makes sense, because you can always trust a value from who lives longer, without concerning about the value somehow goes invalid. What’s behind the scene is that Rust have a system of subtyping and variance.
Subtype
Just think of an example in Java, class Cat
is a subtype of the class Animal
.
In brief, we say T
is a subtype of U
(notation T <: U
) when T
is at least as useful as U
. T
can do anything that U
can do, but T
may have the ability of other things.
In Rust, the lifetime ’static
is a subtype of every lifetime. Rust compiler then uses different variance rules to check whether the program should be compiled or not.
Variance
You may understand variance in many other programming languages. And there are three types of variance in programming, called covariance, contra-variance, and invariance.
Covariance
Covariance is the most common case. Most things in Rust is covariance. For example,
/// define a function which takes an lifetime sticker `a`
fn foo(_: &'a str) {}
// and you can call the function with
foo(&'a str);
// or
foo(&'static str);
In the example, we can give the function with parameter whose lifetime is no matter a
or static
. That is because static
is a subtype of a
.
The static str lives longer than the required ’a
, so there should be no concern that the borrowed variable would be unexpectedly dropped.
Contra-variance
Let’s consider the high-rank function example below.
/// define a function which takes a function,
/// which takes a lifetime sticker `a`.
fn foo(bar: Fn(&'a str) -> ()) {
bar(str);
}
let x : Fn(&'a str) -> ();
foo(x); // that makes sense.
let y : Fn(&'static str) -> ();
foo(y); // should that make sense ???
Should foo(y)
be compilable? Definitely not! Let’s say if foo(y)
compiles, then we are actually doing things in the high-rank function like:
let baz = &*String::new();
// lifetime of baz is shorter than 'static
fn y(_: &'static str) {}
// an function that needs a static borrowing
y(baz);
// [!!] Should not compile
// because a static lifetime is required.
The caller gives a parameter with limited lifetime. But the function we get requires a static lifetime parameter. That cannot be allowed. However, let’s consider another example:
/// define a function which takes a function,
/// which takes a parameter with static lifetime.
fn foo(bar: Fn(&'static str) -> ()) {
bar("Hello Whexy~");
}
let x : Fn(&'static str) -> ();
foo(x); // that makes sense.
let y : Fn(&'a str) -> ();
foo(y); // that makes sense too.
This example is perfect compiled. Because the function y
requires a parameter with limited lifetime. The caller gives it a static parameter which lives longer. Again, there should be no concern that the borrowed variable would be unexpectedly dropped.
So the contra-variance is a specific rule.
T <: U ==> Fn(U) <: Fn(T)
Invariance
Invariance means “no variance”. In a short word, “just pass me exact the thing I require, no tricks.” Let’s see this example:
fn foo(s: &mut &'a str, x: &'a str) {
*s = x;
}
let mut x = "Hello"; // x : &'static str
let z = String::new();
foo(x, &z); // foo(&'static str, &'z str)
drop(z);
println!("{}", x); // OOPS!
The code cannot compile, because we are going to access x
, which points to a dropped memory area. In fact, in &'a mut T
, T
is invariance. However, the 'a
is covariance. It’s not hard to figure out, so I’m left this to you as an exercise.
Reference: variance of types
Variance of types are listed in “The Rustonomicon”
© LICENSED UNDER CC BY-NC-SA 4.0