1.12.1. 生命周期的变型
变型是 Rust 类型系统中的一个概念。它描述了泛型参数(尤其是生命周期参数)在类型层次结构中如何相互关联。
我们可以简单地理解为变型描述了哪些类型是其他类型的“子类型”,其中“子类型”与 Java 和 C# 中的概念有些相似。
此外,变型还关心何时可以用“子类型”替换“超类型”,反之亦然。
一般来说,如果 A 是 B 的子类型,那么 A 至少与 B 一样有用。下面是一个 Rust 示例:如果一个函数接受 &'a str,那么就可以传入 &'static str。因为 'static 是 'a 的子类型,所以 'static 的存活时间至少与任何 'a 一样长(并且 'static 可以保持整个程序的有效性)。
1.12.2. 三种生命周期变型
所有类型都具有变型。每个类型所关联的变型定义了哪些相似的类型可以在该类型的位置上使用。
注意:以下内容较为困难。建议您先回顾高中数学中的充分条件和必要条件。
1. 协变
协变意味着类型只能被“子类型”替换。
协变意味着:
if A <: B (A 是 B 的子类型),则 F<A> <: F<B>(F<A> 也是 F<B> 的子类型)
Enter fullscreen mode Exit fullscreen mode
这是一种从较小到较大的传递继承关系,类似于从充分条件推理:如果 A 成立,则 B 也一定成立(A 是 B 的充分条件)。
例如,&'static T 可以替换 &'a T,因为 &T 对生命周期 'a 是协变的,因此 'a 可以被其子类型(如 'static)替换。
2. 不变
不变意味着必须提供完全指定的类型。
不变意味着:
A <: B 不能推导出 F<A> <: F<B>,F<B> <: F<A> 也无法推导
Enter fullscreen mode Exit fullscreen mode
这意味着 F<A> 与 F<B> 之间没有足够的关系来相互推导,因此它们既不是充分条件也不是必要条件;它们是独立的。
例如,可变引用 &mut T 对 T 是不变的。
3. 逆变
逆变意味着:
if A <: B(A 是 B 的子类型),则 F<B> <: F<A>(F<B> 反而是 F<A> 的子类型)
Enter fullscreen mode Exit fullscreen mode
这里的逻辑是:“要使 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_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);
}
Enter fullscreen mode Exit fullscreen mode
这段代码中令人困惑的部分是 MutStr 结构体,让我们分解一下:
- 该结构体只有一个字段,但有两个生命周期
-
&'a mut表示可变引用,该可变引用的生命周期为'a -
&'b str表示对字符串切片的引用,该字符串切片的生命周期为'b - 换句话说,
MutStr允许您存储一个指向字符串切片引用的可变引用。您可以修改s本身,但不能修改&'b str所指向的字符串内容
接下来,让我们看看 main 中的逻辑:
let mut s = "hello";声明了变量s,其类型为&str,值为"hello"-
*MutStr { s: &mut s }.s = "world";实际上是将几步操作合并为一行。让我们将它们分开:-
MutStr { s: &mut s }将s的可变引用传入MutStr结构体。此时,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);
}
Enter fullscreen mode Exit fullscreen mode
输出:
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
Enter fullscreen mode Exit fullscreen mode
在这一行代码上,rustc 在赋值处报告失败(expected str, found &str,并且 str 的大小未知)。更深层次的类型问题是 &mut s 的类型是 &mut &str,而字段期望的是 &mut str。
更具体地说:
- 变量
s的类型是&str(对字符串切片的引用)。 - 当您编写
&mut s时,其实际类型是&mut &str,即对变量s的可变引用。但是,MutStr的定义要求字段s的类型为&mut str。 - 如果单独构造
MutStr { s: &mut s },rustc 会报告无法将&引用背后的数据作为可变借用——这是同一个底层不匹配,以不同的方式显现。
这些是不同的所指类型,而不是生命周期子类型的问题。(另外,请注意 &mut T 确实 支持非确定大小的强制转换,例如 &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,因此类型不匹配
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.