1.12 生命周期(进阶) Pt.2:生命周期变型(Lifetime Variance)、协变(covariant)、不变(invariant)、逆变(contravariant)
这篇文章在已有基础上对生命周期这一概念进行补充;上篇见 1.11. 生命周期(进阶) Pt.1。
1.12.1. 生命周期变型(Lifetime Variance)
变型(Variance)是Rust类型系统中的一个概念,它描述了泛型参数(特别是生命周期参数)在类型层次结构中的继承关系。
我们可以简单地理解为变型是用于描述哪些类型是其他类型的“子类”的,这里的“子类”比较类似于Java和C#中的子类。
除此以外,变型还会关注什么时候“子类”能够替换“超类”(反之亦然)。
通常来说,如果A是B的子类,那么A至少和B一样有用。举一个Rust语言的例子:如果函数接收&'a str的参数,那么就可以传入&'static str的参数。因为'static是'a的子类,'static至少跟任何'a存活的时间一样长('static能够在程序运行中一直保持有效)。关于这个生命周期详见 1.6.2. 'static生命周期标注。
1.12.2. 生命周期的3种变型
所有类型都有变型,每个类型所对应的变型定义了哪些类似类型可以用在该类型的位置上。
注意:以下的内容比较难,建议你先回忆一下高中学的充分条件、必要条件等知识
1. 协变(covariant)
协变(covariant)指的是某类型只能用“子类型”来代替。
协变表示:
如果 A <: B(A是B的子类型),那么 F<A> <: F<B>(F<A>也是F<B>的子类型)
这是一个从小到大继承关系的传递,类似于充分条件的推导:如果 A 成立,则 B 一定成立(A 是 B 的充分条件)。
例如&'static T可以代替&'a T,因为&T是对'a这个生命周期的协变,所以'a可以由它的子类(比如说'static)来替代。
2. 不变(invariant)
不变(invariant)意味着必须提供指定的类型。
不变表示:
A <: B 不能推导出 F<A> <: F<B> 也不能 F<B> <: F<A>
这意味着F<A>和F<B>之间没有足够的关系,无法形成推导关系,因此它们既不是充分条件,也不是必要条件,它们是独立的。
比如说&mut T这个可变引用对于T来说就是不变的。
3. 逆变(contravariant)
逆变表示:
如果 A <: B(A是B的子类型),那么 F<B> <: F<A>(F<B>反而是F<A>的子类型)
这里的逻辑是 “想要F<A>成立,B必须满足A的条件”,更像必要条件:如果 B 成立,则 A 也必须成立(A 是 B 的必要条件)。
你可以把逆变理解为“成反比”:函数吧对参数的要求越低,参数可发挥的作用越大。
举两个例子:
-
假如有两个变量
x1和x2,其中x1的生命周期是'static,x2的生命周期是'a。那么毫无疑问x1的作用比x2大,因为它活得更长。 -
假如有两个函数
take_func1和take_func2,其中take_func1接收的参数是&'static str,take_func2接收的参数是&'a str。毫无疑问,take_func1对参数的要求比take_func2严格,这就导致了take_func1没有take_func2作用大。
由上述的两个例子看出,给变量声明一个更长的生命周期会使它的作用更大,但是要求函数的参数是更长的生命周期就会使函数的作用更小。这就是所谓的逆变。
那么这叫谁和谁的逆变呢?就是函数对它里面的参数类型的逆变。
1.12.3. 生命周期变型的作用
我们通过一个例子来看一下生命周期变型的作用:
struct MutStr<'a, 'b> {
s: &'a mut &'b str,
}
fn main() {
let mut s = "hello";
*MutStr { s: &mut s }.s = "world";
println!("{}", s);
}
这个代码令人比较困惑的地方在于MutStr这个结构体,我们来解析一下:
- 这个结构体只有一个字段,但是拥有两个生命周期
&'a mut表示 一个可变引用,这个可变引用的生命周期是'a&'b str表示 一个字符串切片的引用,这个字符串切片的生命周期是'b- 换句话说,
MutStr允许你存储一个可变引用,这个引用指向一个字符串切片的引用。你可以修改s本身,但不能修改&'b str指向的字符串内容
接下来我们来看一下主函数的逻辑:
-
let mut s = "hello";声明了s这个变量,类型是&str,值是“hello“ -
*MutStr { s: &mut s }.s = "world";这其实是好几步被合在了一行,我们分开来看:MutStr { s: &mut s }传递给MutStr结构体s的可变引用,此时MutStr下的s字段的值就是“hello“*MutStr { s: &mut s }.s = "world";的.s表示访问s字段(此时这个字段的值是&mut s)。*解引用s,即获得s这个字符串切片的引用本身。= "world"修改了指向的值——s之前指向“hello“,现在被修改为“world“,即s = "world"。
那如果只有一个生命周期还能这么写吗?
struct MutStr<'a> {
s: &'a mut str,
}
fn main() {
let mut s = "hello";
*MutStr { s: &mut s }.s = "world";
println!("{}", s);
}
输出:
error[E0308]: mismatched types
--> src/main.rs:7:31
|
7 | *MutStr { s: &mut s }.s = "world";
| ----------------------- ^^^^^^^ expected `str`, found `&str`
| |
| expected due to the type of this binding
error[E0277]: the size for values of type `str` cannot be known at compilation time
--> src/main.rs:7:5
|
7 | *MutStr { s: &mut s }.s = "world";
| ^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `str`
= note: the left-hand-side of an assignment must have a statically known size
在这行合写的代码上,rustc 把失败报在赋值处(expected str, found &str,以及 str 不是 Sized)。更深层的类型问题是:&mut s 的类型是 &mut &str,但字段期望的是 &mut str。
具体来说:
- 变量
s的类型是&str(引用一个字符串切片)。 - 当写
&mut s时,其类型实际上是&mut &str,即对变量s的可变引用。然而,结构体MutStr的定义要求字段s的类型是&mut str。 - 如果把构造单独写成
MutStr { s: &mut s },rustc 则会报告:不能把&引用背后的数据再可变借用——这是同一类底层不匹配,只是报错位置不同。
这是所指类型不同,而不是生命周期子类型问题。(另外需要注意:&mut T确实支持 unsizing 强制转换,例如&mut [T; N] → &mut [T];但这种机制仍然不能把&mut &str变成&mut str。)
不变性真正起作用的地方是双生命周期版本:&'a mut &'b str 在 'b 上是不变的,这可以防止你通过可变引用赋值时,以不安全的方式缩短内部借用。
也可以这么理解:
- 字符串字面值(“hello“和“world“就是字符串字面值)是
&str类型,有隐式的'static生命周期标注,也就是说&str实际上是&'static str。原本的结构体里的'b对应它 - 结构体的
'a对应可变引用的生命周期,也就是*MutStr { s: &mut s }.s = "world"这一行中的&mut这个可变引用的生命周期 - 修改之后的这个只有一个生命周期参数的结构体期望的是
&mut str,但&mut s的类型仍是&mut &str,因此类型不匹配