喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
这篇文章在Rust初级教程的基础上对生命周期这一概念进行了补充,建议先看【Rust自学】专栏的第10章的文章。
1.12.1. 生命周期变型(Lifetime Variance)
变型(Variance)是Rust类型系统中的一个概念,它描述了泛型参数(特别是生命周期参数)在类型层次结构中的继承关系。
我们可以简单地理解为变型是用于描述哪些类型是其他类型的“子类”的,这里的“子类”比较类似于Java和C#中的子类。
除此以外,变型还会关注什么时候“子类”能够替换“超类”(反之亦然)。
通常来说,如果A是B的子类,那么A至少和B一样有用。举一个Rust语言的例子:如果函数接收&'a str
的参数,那么就可以传入&'static str
的参数。因为'static
是'a
的子类,'static
至少跟任何'a
存活的时间一样长('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
这个字符串切片的引用本身。= "hello"
修改了指向的值——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);
}
这段代码的问题在于类型不匹配和 Rust 中可变引用的不变性(invariance)。
具体来说:
- 变量
s
的类型是&str
(引用一个字符串切片)。 - 当写
&mut s
时,其类型实际上是&mut &str
,即对变量s
的可变引用。然而,结构体MutStr
的定义要求字段s
的类型&mut str
。
虽然不可变引用(&T
)支持“unsizing coercion”(例如可以将&[T; N]
自动转换为&[T]
),但是 可变引用 (&mut T
) 是不变(invariant)的,这意味着Rust不允许自动将&mut &str
转换为&mut str
。
因此,编译器会报类型不匹配的错误,因为它无法将&mut s
(类型&mut &str
)传给MutStr { s: ... }
期望的&mut str
参数。
在这里解释一下不变性:
- 不可变引用 (
&T
) 是协变(variant)的:允许自动的unsizing转换,比如可以将&[T; N]
当作&[T]
使用 - 可变引用 (
&mut T
) 是不变(invariant)的:这就意味着&mut &str
与&mut str
被视为完全不同的类型,编译器不会自动进行转换
也可以这么理解:
- 字符串字面值("hello"和"world"就是字符串字面值)是
&str
类型,有隐式的'static
生命周期标注,也就是说&str
实际上是&'static str
。原本的结构体里的'b
对应它 - 结构体的’a对应可变引用的生命周期,也就是
*MutStr { s: &mut s }.s = "world"
这一行中的&mut
这个可变引用的生命周期 - 修改之后的这个只有一个泛型生命周期的结构体无法同时对应这两个生命周期标注,因为可变引用
&mut T
是不变(invariant)的,&mut &str
与&mut str
被视为完全不同的类型,所以导致了报错