rust 生命周期

Created

2024-05-04 09:49:42

Updated

2024-10-30 15:21:35

1 先看个例子

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }    // x 离开作用域,其值被丢弃

    println!("r: {}", r);
}
cargo run 
 --> src/main.rs:6:13
  |
5 |         let x = 5;
  |             - binding `x` declared here
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {}", r);
  |                       - borrow later used here

rust编译器 如何发现问题的呢?

2 借用检查器

The Borrow Checker

Caution

借用的生命周期必须比 所有者的生命周期 短, 就是 所有者离开作用域前,你才可以使用它的借用

rust编译器 有一个 borrow checker 它会比较作用域来判断所有借用是否合法

标注上变量的生命周期,我们看看

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

这里用 ’a 表示 变量r 的生命周期, ’b 表示变量x 的生命周期 变量r (借用了x)的生命周期 ’a 比 所有者x 的生命周期 ’b 长 , 而这个时候x的生命周期 ’b 已经结束了,x不可用了

将上面的代码修改成如下,就ok了

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

3 生命周期的标注

3.1 为什么需要

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = "xyz".to_string();
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}
分析一下上面例子
  1. longest返回一个借用, 那么我们必须知道, 这个返回值(调用方的接收者)借用的值的生命周期要比这个借用长,编译器需要能够检查是否达到这样的要求
  2. 因为函数内的局部变量在函数执行完毕后, 都会失效的, 所以返回值是借用的情况下, 它肯定只能是来至于函数参数且该参数必定是引用类型 (这里 暂不考虑 &’static ),只要知道它来至于哪个, 就能确定接收者和谁的生命周期有关
  3. 现在所有问题的关键在于编译器不知道longest函数返回值来至于哪个参数,而生命周期标注最终就是为了让编译器知道它返回的引用与谁有关,这样就知道最后需要和谁的生命周期比较, 这样就帮助了编译器进行静态分析,去防止悬垂指针和其他内存安全问题
  4. 生命周期标注 只是描述多个借用之间的关系, 并不会改变他们的生命周期
  5. 上面的例子中编译器不知道返回哪个, 意味着编译器需要将2种情况都考虑,使用情况最坏的哪个,就是生命周期最短的哪个, 因为你不可能让编译器强制认为返回其中一个,让编译通过吧? 例子中 最坏的情况是返回 string2的借用, 因为string2 生命周期很短,会导致result 悬垂引用

3.2 标注的真实含义

Note
  • 只有引用类型才需要标注
  • 这个就类似类型来约束你申请的内存, 生命周期标注是表示你的生命周期在哪个范围 , 可以理解为: 生命周期标注表示生命周期的类型

3.3 函数

前置知识: 什么是函数的签名(Function Signature)

在编程中, 是指函数的定义中描述函数接口的部分. 它通常包括以下几个方面:

  1. 函数名: 函数的名称,用于标识和调用函数
  2. 参数列表: 函数接受的参数及其类型. 参数列表定义了函数可以接收的数据类型和数量
  3. 返回类型: 函数返回值的类型.如果函数不返回值,通常会标记为 void(在 C/C++ 中)或省略返回类型(在 Rust 中为 ())
  4. 生命周期(在 Rust 中): Rust 中的函数签名还包括生命周期注解,用于描述引用之间的生命周期关系

使用 ’a ’b 这种 单个简单字母, 放在 &后面

&i32        // 一个借用
&'a i32     // 一个带有显式生命周期的借用
&'a mut i32 //

也可以先去看 covariant-协变, 早期还没看covariant相关教程,只是看完book那个教程, 浪费了很多思考, 教程分不同批次,对于我来说是恶心的.

报错了
fn longest(x: &str, y: &str) -> &str {
    x
}
fn main() {
    let string1 = String::from("long string is long");
    let result;
    let string2 = String::from("xyz");
    result = longest(string1.as_str(), string2.as_str());
}
分析
  • 上面这个例子, 你可能最开始有这样的疑惑,这不是很明显返回的是x吗,还需要标注生命周期吗?
  • 提示错误说函数签名没有标注生命周期
  • 高亮第8行处, 实际上编译器在编译到这里的时候,它是只能根据函数签名的信息来判断返回值来至于哪里,所以它不知道该函数返回值和谁有关了., 我们继续看例子2
还是报错了
// 生命周期标注表示返回值与y 有关, 表示 返回值的生命周期必须符合 `'a`这样长的范围,至少和y 一样长
fn longest<'a>(x: &str, y: &'a str) -> &'a str {
    x
}
fn main(){}
分析
  • 好了,这样我们标注了 返回值和y有关, 唉? 这是不是感觉就行了啊?
  • 报错了, 因为编译器编译longest函数, 它确实肯定是知道返回值是x的, 然而我们设置的返回值类型是 &'a str, x能否转换为这个类型?
    • 不行!, 因为x的标注是 'b (没写就是默认自己的生命周期,这里就用'b表示) 与'a 没有指明任何关系, &'b str 类型 不能转换为 &'a str
  • 把’a 生命周期标注作为类型的一部分
// 从代码上看 y和返回值 没有任何关系, 那么我们不需要标注
// 表示 返回值的生命周期必须符合 'a这样长的范围
// 就这个例子而言, 编译器编译这个函数时是知道它返回 了 x
// 而x 符合 返回类型 &'a str , 本身就是 'a 这样的长度
// 'a 的长度或说范围 是实际调用该函数时传递的参数的生命周期, 实际调用处是string1 是main函数都有效的生命周期
// 然后实际调用处接收者 result的生命周期 比如说是 'c , 将 'a 类型的 赋值给 'c 类型的 能否成功,  这里是可以的, 他们生命周期长度一样.
// 赋值相当于类型转换, 'a 类型 转换为 'c 类型, 'a 如果是 'c 的子类 就可以了, 表示 'a的生命周期 >= 'c
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        // 注意编译器编译到这里, 它是不关注函数体的, 它只会关注到函数的签名
        // 如果函数签名没有生命周期标注,它根本不知道返回值和哪个参数有关.
        // 现在发现有了生命周期标注, 表示返回值和string1有关系
        // 那么 string2的死活与它无关了.
        result = longest(string1.as_str(), string2.as_str());
    } // 这个时候 string2 指向的 xyz (在堆上) 已经被free了
      // 而编译器知道 longest 返回的是来至于第一个参数x: string1.as_str()
      // 这个所谓的知道是通过 生命周期标注 知道的
      // ok
    println!("The longest string is {}", result);
}
Caution

例子2里我们说 ’b 因为没有指明 和’a的关系, 所以他们之间不能转换, 那么他们需要什么样的关系才能转换能?

// 设置返回值生命周期 约束为 'a , 表示至少和y 一样长
// 但是实际返回值是x, 它的生命周期 是 'b
// 'b 能不能转为 'a  就是问题的关键了. 当'b 是 'a的子类就可以了,('b:'a) 表示'b 生命周期比 >= 'a
// 子类, 就是继承了父类 所有特征的类, 所以它可以转换为 父类, 这个好理解的(这里是子类生命周期比父类长)
fn longest<'a,'b:'a>(x: &'b str, y: &'a str) -> &'a str {
    x
}
fn longest2<'a, 'b>(x: &'b str, y: &'a str) -> &'a str
where // 和泛型 使用方式类似, 可以这样写
    'b: 'a,
{
    x
}
fn main(){
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        // string1 的生命周期比 string2 长
        // 为了符合 签名中 'b 要>= 'a , 只能转换为 'b = string2的生命周期
        // 这样 'b = 'a  符合 'b:'a
        // 所以实际返回的x 它的生命周期变成了 string2 一样了,
        // 当然根据签名我们直接知道与string2有关,
        //      它的生命周期就是string2的生命周期,因为string2 能活多久,它就多久
        // 编译直接报错,提示string2, borrowed value does not live long enough
        // 从代码角度看, 返回的值的生命周期和 string2一样, 而result的生命周期 比这个长,
        // 无法从一个 短的生命周期 赋值给一个长的生命周期, 可理解为父类无法转换为子类
        result = longest(string1.as_str(), string2.as_str());
    } // string2 释放
    println!("The longest string is {}", result);
}
报错了
// 生命周期标注表示返回值与x,y有关, 表示 返回值的生命周期必须符合 'a这样长的范围
// 返回值 x 符合 'a 类型的生命周期 编译通过
// 传递来的时候, 确定出 'a =x的实参的生命周期 , 然后又确定出 'a=y的实参的生命周期
// 最终是 'a = x和y 实参生命周期短的那个
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    x
}
fn main(){
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        // 注意编译器编译到这里, 它是不关心函数体的, 它只会关注到函数的签名
        // 所以它根本不知道返回值和哪个参数有关.(虽然函数内部很明显返回了谁)
        // 现在看了函数签名, 发现有了生命周期标注, 表示string1,string2有关系
        // 从人的角度看, 可以理解为, 编译器在这种情形下会考虑最坏的情况,
        // 所以这里认为返回值 返回生命周期短的那个, 就是string2,所以最终和string2有关
        // 从代码角度看, 返回的值的生命周期是 string2, 而result的生命周期 比这个长,
        // 无法从一个 短的生命周期 赋值给一个长的生命周期, 可理解为父类无法转换为子类.
        result = longest(string1.as_str(), string2.as_str());
    } // 这个时候 string2 指向的 xyz (在堆上) 已经被free了
    // 报错了
    println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = "xyz";
        result = longest(string1.as_str(), string2);
    } // string2 所借用的 xyz 这个数据实际是在 只读区中分配的,生命周期是到程序运行结束,所以到这里,没有被free掉
      // println!("{}", string2); 注意这个还是会报错的, 因为变量string2 离开作用域 已经不可用了
      // OK
    println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    } 
    println!("The longest string is {}", result);
}
生命周期的总结
  1. 返回值的 ’a 标注 如果与多个函数参数有关, 那么选择 生命周期短的那个参数, 表示 返回值与 该参数有关, 最后借用检查就知道返回值的生命周期和谁做比较
  2. 如果返回值 ’a 就和一个参数有关, 那么就和它有关
// 不带参数的函数, 不过有一个生命周期参数 'a
// 'a: 表示至少和函数的生命周期一样长 : >=
fn failed_borrow<'a>() {
    let _x = 12;

    // 报错:_x 的生命周期不够长
    let y: &'a i32 = &_x;
    // 在函数内部使用生命周期 'a 作为显式类型标注将导致失败
    // 因为 &_x 的生命周期比 y 的短. 短生命周期不能强制转换成长生命周期.
}
fn main(){}

3.4 结构体

Important
  • 生命周期标注 ’a 表明这两个字段的生命周期是相同的,并且它们至少活得和 Book 实例一样久
  • 如果没有生命周期标注,编译器就无法确定title 和 author 引用的数据是否会在 Book 实例化之前被释放
  • 生命周期标注提供了一种方式,让编译器能够检查引用是否在有效范围内,从而确保内存安全
struct Book<'a> {
    title: &'a str,
    author: &'a str,
}

fn main() {
    let title = "The Rust Programming Language";
    let author = "Steve Klabnik and Carol Nichols";
    let book = Book { title, author };

    println!("Book title: {}", book.title);
    println!("Book author: {}", book.author);
}

3.5 约束

Important
  • T: ’a
    • 在 T 中的 所有引用都必须至少和生命周期 ’a 活得一样长 (>=’a)
  • T: Trait + ’a
    • T 类型必须实现 Trait trait, 并且在 T 中的所有引用都必须至少和 ’a 活得一样长
  • &’a T
    • ’a 表示T 的生命周期至少和’a 一样长
fn main() {
    let s2 = "hello".to_string();
    test2(&s2); // ok
    test2(&&s2); // 报错了
    test1(&s2) // 报错了 &'static T , T 需要满足'static
}
fn test1<T>(_arg: &'static T) {
    println!("hello");
}
// T: 'static 表示T中使用的引用 必须至少 比'static 一样长
// test2(&s2); 调用的情况 这里T 是 String 类型, 不是引用类型,所以啥都行
fn test2<T: 'static>(_arg: &T) {
    println!("hello");
}

3.6 &’static

Tip

整个程序的持续时间就是静态生命周期

let s: &'static str = "I have a static lifetime.";

3.7 泛型,trait

Important

lifetimes are a type of generic,所以会将它和泛型T的申明放在一起

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

3.8 标注省略三规则

// 早期像这样的,都是需要 进行生命周期标注的
// fn first_word(s: &str) -> &str {
fn first_word<'a>(s: &'a str) -> &'a str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}
三个规则
  • rust的开发人员注意到, 每次都要写很多这种标注, 很麻烦, 于是总结了一些规则,让编译器自动处理
  • 概念说明
    • 生命周期在函数的参数时,被称为输入生命周期
    • 生命周期在函数的返回值时,被称为输出生命周期
  • 三个规则
    1. 每个引用参数都有自己的生命周期参数
    2. 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
    3. 如果有多个输入生命周期参数, 且其中一个是&self 或 &mut self(是方法), 则self的生命周期会被赋予所有的输出生命周期参数
  • 如果编译器应用完这3个规则,还有无法确定生命周期,那么就报错
// 我们写的代码
fn first_word(s: &str) -> &str {}
// 1. 编译器应用第一条规则后
fn first_word<'a>(s: &'a str) -> &str {}
// 2. 编译器应用第二条规则后
fn first_word<'a>(s: &'a str) -> &'a str {}

// 我们写的代码
fn longest(x: &str, y: &str) -> &str {}
// 1. 编译器应用第一条规则后
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
// 2. 编译器应用第二条规则,发现有多个输入参数,pass
// 3. 编译器应用第三条规则,发现是个函数,不是方法,pass
//  结果, 返回值的生命周期 还是不确定, 所以报错了

结构体方法生命周期

// 需要在 impl后面申明,这个生命周期是结构体类型的一部分
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}
// 1. 编译器应用第一条规则, &'a self, 所以我们不用标注了

// 我们来看看方法中多个参数的情况,
impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}
// 1. 编译器应用第一条规则后,
    fn announce_and_return_part(&'a self, announcement: &'a str) -> &str {}
// 2. 编译器应用第三条规则后,
    fn announce_and_return_part<'a>(&'a self, announcement: &'a str) -> &'a str {}

4 drop check

5 subtyping1

Important
  • 在前面章节中 我们看到下面这样的代码
  • 这里 其实明确表示 需要传递的参数的 生命周期是一样长的,然而在实际调用的时候, 传递的参数的生命周期不一样,为什么可以这样呢
  • 还有返回值的生命周期类型 和接收者的生命周期类型 也是不一样的, 为什么也可以成功赋值呢
  • 之前就说到子类可以转换为 父类,rust 提供这样的机制,可以让子类生命周期(长生命周期)降级转换为父类生命周期(短生命周期), 所以编译不会报错
// x 生命周期长的情况, 就降级转换为 和 y一样的短的生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    x
}
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

6 Variance 型变2

Type Variance in ’a Variance in T
&'a T covariant covariant
&'a mut T covariant invariant
Box<T> covariant
Vec<T> covariant
*const T covariant
*mut T invariant
[T] and [T; n] covariant
fn() -> T covariant
fn(T) -> () contravariant
std::cell::UnsafeCell<T> invariant
std::cell::Cell<T> invariant
dyn Trait<T> + 'a covariant invariant

6.1 covariant 协变

Tip
  • 协变指的是在类型层次中, 子类型可以替代父类型
  • T:U(T是U的子类) 时, F<T> 如果也是 F<U>的子类,那么就称为 F<T> 对于参数T 是协变关系
  • 在rust中, 生命周期有这类机制
协变规则
  • &'a T 对于 'a 是协变的, 当 'a:'b ,那么 &'a T 也是 &'b T 的子类
例子说明
// 要求 2个借用变量的生命周期一样长, 而实际上传递的参数的生命周期不一样
// rust 会通过将长的生命周期的参数 降级为 短生命周期参数一样长的 生命周期
fn debug<'a>(a: &'a str, b: &'a str) {
    println!("a = {a:?} b = {b:?}");
}

fn main() {
    let hello: &'static str = "hello";
    {
        let world = String::from("world");
        // 'world 生命周期短于 'static
        let world = &world;
        // hello 隐式的从 &'static str 降级 或者说转换为 &'world str
        // &'static str是  &'world str 的子类, 所以可以执行成功
        debug(hello, world);
    }
}
协变规则
  • &'a T 对于 T 是协变的, 当 T:U ,那么 &'a T 也是 &'a U 的子类
例子说明
// 这里T 是 &'b str ,U 是 &'a str, 根据前面的因为'b:'a 则 &'b str 是&'a str 的子类
// 现在程序OK, 就说明了, &'a &'b str 是 &'a &'a str 的子类了
fn covariant_ref<'a, 'b: 'a>(x: &'a &'b str) -> &'a &'a str {
    x
}

fn main() {
    let x: &'static str = "hello";
    let y: &&str = covariant_ref(&x);
    println!("{}", y);
}
Important
  • 下面例子中的test1, 我不知道网上是否有人也这样测过, 一开始根据参数的推理,我是很快就想到的,不过返回值的推理没有马上注意到,可以看看下面的注释.
// 根据前面结构体章节, 我们知道 该结构体对于T 和 'a 都是协变的
struct MyStruct<'a, T: 'a> {
    ptr: &'a T,
}
// 编译通过, 可能没想到 这个竟然能通过
// 看参数 我们知道T:'a 表示T 中引用必须至少和'a 一样长,
// 传递的T = &'b str, 这个时候T的生命周期是'b,
// 它必须活着至少>= 'a ,则MyStruct<'a, &'b str> 推断出  'b:'a
// 看返回值 MyStruct<'b, &'a str>,T的生命周期是'a, 它至少和'b 一样长, 即推断出: 'a:'b
// 现在'a:'b  'b:'a  , 其实 'a='b
fn test1<'a, 'b>(v: MyStruct<'a, &'b str>) -> MyStruct<'b, &'a str> {
    v
}
// 看看下面4个测试 来间接说明我们test1 的判断
// 这个符合我们的判断对于 参数'a 和T 都是协变的, 很容易理解的版本
fn test2<'a: 'c, 'b: 'd, 'c, 'd>(v: MyStruct<'a, &'b str>) -> MyStruct<'c, &'d str> {
    v
}

// 很容易得出 我们需要 'b:'c 才能通过编译
fn test3<'a, 'b: 'c, 'c>(v: MyStruct<'a, &'b str>) -> MyStruct<'a, &'c str> {
    v
}
// 现在我们看看这个,'b:'c 才能通过编译, 而这里我们用 'a:'c 也能通过, 间接说明 'b:'a
fn test4<'a: 'c, 'b, 'c>(v: MyStruct<'a, &'b str>) -> MyStruct<'a, &'c str> {
    v
}
// 通过编译的.
// 参数推断出 'a:'b  , 返回值推断出 'b:'c
// 刚好符合 我们的要求, 直接不用设置了
fn test5<'a, 'b, 'c>(v: MyStruct<'b, &'a str>) -> MyStruct<'c, &'b str> {
    v
}
fn main(){}
协变规则
  • &'a mut T 对于 'a 是协变的
例子说明
// 'b: 'a  ,这里没有报错, 则表示 &'b mut T 也是  &'a mut T 的子类
fn invariant_mut<'a, 'b: 'a, T>(x: &'b mut T) -> &'a mut T {
    x
}

fn main() {
    let mut x = 5;
    let y = invariant_mut(&mut x);
    *y += 1;
    println!("{}", x);
}
协变规则
  • Box<T> 对于 T 是协变的
例子说明
fn set_box_ref<'a>(x: Box<&'static str>) {
    let s: Box<&'a str> = x;
    println!("{s}");
}

fn main() {
    let hello: Box<&'static str> = Box::new("hello");
    set_box_ref(hello);
}
协变规则
  • fn() -> T 对于 T 是协变的
例子说明
fn set_fn_ret<'a>(f: fn() -> &'static str) {
    let _: fn() -> &'a str = f;
}
// 直接报错的, 说明不是逆变的
// fn set_fn_ret2<'a>(f: fn() -> &'a str) {
//     let _: fn() -> &'static str = f;
// }
fn main() {
    fn test() -> &'static str {
        "hello"
    }
    set_fn_ret(test);
}

6.2 Contravariance 逆变

Tip

逆变指的是在类型层次中, 父类型可以替代子类型

逆变规则
  • fn(T) -> () 对于 T 是逆变的
// OK , 说明是逆变的
fn set_fn_arg<'a>(f: fn(&'a str)) {
    let _: fn(&'static str) = f;
}
// 报错了, 不是协变
fn set_fn_arg2<'a>(f: fn(&'static str)) {
    let _: fn(&'a str) = f;
}
fn main() {}

6.3 invariant 不变

Important

不是协变也不是逆变的情况, 就是不变

不变规则
  • &'a mut T 对于 T 是不变的, 这意味着你不能将 &'a mut T 隐式地转换成 &'a mut U 即使 TU 的子类型或者父类型
  • 和 协变规则2 比较一下
例子说明
// 前面已经验证 &'b str 是&'a str 的子类
// T 就是 &'b str ,U 是 &'a str
// 然而编译报错, 说明了&'a mut &'b str 不是 &'a mut &'a str 的子类
// vscode 提示的错误 ,可以了解到 &'a mut T 对于T 是不变的
fn covariant_ref<'a, 'b: 'a>(x: &'a mut &'b str) -> &'a mut &'a str {
    x
}

fn main() {
    let mut x: &str = "hello";
    let y: &&str = covariant_ref(&mut x);
    println!("{}", y);
}

6.4 自定义类型的情况

看下面代码
  1. 如果所有使用泛型A 的成员 对A 都是 协变的, 那么 MyType 对于A 就是 协变的
  2. 如果所有使用泛型A 的成员 对A 都是 逆变的, 那么 MyType 对于A 就是 逆变的
  3. 否则就是不变的
use std::cell::Cell;
struct MyType<'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H, In, Out, Mixed> {
    a: &'a A,     // covariant over 'a and A
    b: &'b mut B, // covariant over 'b and invariant over B

    c: *const C,  // covariant over C
    d: *mut D,    // invariant over D

    e: E,         // covariant over E
    f: Vec<F>,    // covariant over F
    g: Cell<G>,   // invariant over G

    h1: H,        // would also be covariant over H except...
    h2: Cell<H>,  // invariant over H, because invariance wins all conflicts

    i: fn(In) -> Out,       // contravariant over In, covariant over Out

    k1: fn(Mixed) -> usize, // would be contravariant over Mixed except..
    k2: Mixed,              // invariant over Mixed, because invariance wins all conflicts
}
举例 a: &’a A
  1. 注意 &'b Cat<'c, T> 对于 'b 是协变的(将 Cat<'c, T> 看成一个整体 就是前面说过的 &'a T 对于 'a 是协变的 )
  2. Cat<'c, T> 因为成员对于 'c 是协变的, 所以 Cat<'c, T> 对于 'c 也是协变的
运行OK
fn covariant_mut<'a, 'b: 'a, 'c: 'd, 'd, T>(x: &'b Cat<'c, T>) -> &'a Cat<'d, T> {
    x
}

struct Cat<'a, T> {
    name: &'a T,
}
fn main() {}

7 PhantomData 类型3

phantom: 幽灵, 无形的东西, 假象,虚构的事物 PhantomData 翻译为 幽灵数据 好了…

7.1 为什么需要4

struct Iter<'a, T: 'a> {// 没有使用到生命周期参数 'a
    ptr: *const T,
    end: *const T,
}
struct Dog<T> { // 没有使用到类型参数 T
    data: i32
}
分析
  • 自定义类型的情况中,我们提到自定义类型对泛型参数A(生命周期也是一种泛型参数)的型变关系需要通过成员对A的型变关系来推断, 但是上面代码struct Iter<'a,T:'a>申明了生命周期参数, 但是实际光有申明又没法使用它(裸指针本身不带生命周期参数), 这样导致无法知道该类型的型变关系, 因此编译器会报错
  • struct Dog<T> 申明了泛型T 但是实际里面没有使用它, Dog<i32>Dog<i64>就没有什么区别, 那么编译器会推断他们是可以互换的,这就很不合理
  • PhantomData 类型可以解决这些问题,std::marker::PhantomData是一个特殊的标记类型, 不占用任何内存空间 ZST
  • 作用
    1. 向编译器提供对静态分析有用的信息, 比如正确处理 泛型参数(T和生命周期)的 型变
    2. 它用于在类型系统中表示和某类型有关, 但实际上并不存储该类型的值, 可以消除未使用类型参数的警告
    3. 控制 Variance (协变,逆变还是不变)

7.2 消除未使用类型参数的错误

use std::marker::PhantomData;
struct Dog<T> {
    data: i32,
    // 这个字段不会增加结构体的内存占用
    // 表示尽管结构体没有使用泛型T, 但是这里就认为它使用了T
    // 像一个幽灵一样, 我们假装有这个一个字段 使用了 T , 就好比一个字段名 _marker:T 这样
    // 只是表明该结构体 和 T 有关
    _marker: PhantomData<T>,
}

7.3 控制Variance

7.3.1 控制 T

Type Variance in T
PhantomData<T> covariant
PhantomData<fn(T)> contravariant
PhantomData<Cell<T>> invariant
Important
  • 使整个结构体对于 T 是协变的
struct Dog<T> {
    data: i32,
    _marker: PhantomData<T>,
}

fn test1<'a: 'b, 'b>(v: Dog<&'a str>) -> Dog<&'b str> {
    v
}
fn main(){}
Important
  • 使整个结构体对于 T 是逆变的
struct MyStruct<T> {
    data: i32,
    _marker: PhantomData<fn(T)>,
}

fn test1<'a, 'b: 'a>(v: MyStruct<&'a str>) -> MyStruct<&'b str> {
    v
}
Important
  • 使整个结构体对于 T 是不变的
  • 根据前面型变表格, 你可以使用其他比如PhantomData<*mut T>
struct MyStruct<T> {
    data: i32,
    _marker: PhantomData<std::cell::Cell<T>>,
}

fn test1<'a: 'b, 'b: 'a>(v: MyStruct<&'a str>) -> MyStruct<&'b str> {
    v
}

7.3.2 控制 ’a

Type Variance in ’a
PhantomData<&'a ()> covariant
PhantomData<fn(&'a ())> contravariant
PhantomData<Cell<&'a ()>> invariant
Important
  • 使整个结构体对于 ’a 是协变的
PhantomData<&'a ()>
use std::marker::PhantomData;

struct MyIter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
    // 我们知道结构体对于'a 的型变关系 取决于 里面的成员对于'a 的型变关系
    // 本来结构体没有使用'a , 现在我们假装有一个成员 是 &'a (), 比如假装一个成员是 name: &'a ()
    // &'a () 对'a 是协变的, 只要该成员使用了'a ,那么整个结构体对于'a 就是协变的了 (因为只有这个成员使用了'a)
    // 而对于T 来说, 看ptr 和end 成员就知道了, 他们对T 是协变的, 所以结构体对于T 也是协变的了
    // 可能有人会说 为什么不用 PhantomData<&'a T> , 因为结构体对于T 的型变规则已经决定了, 由成员ptr 和end 确定
    // 所以你没有这个意图去写成 () , 等下后面的例子我们 改成 mut T 就能更理解了.
    _marker: PhantomData<&'a ()>,
}
// 编译ok
fn test1<'a: 'b, 'b, T: 'a>(v: MyIter<'a, T>) -> MyIter<'b, T> {
    v
}
// 编译ok了,  T这个时候=&'a str  返回值的T 是&'c str
//  说明了  MyIter 对于T 是协变
fn test2<'a: 'c, 'b, 'c>(v: MyIter<'b, &'a str>) -> MyIter<'b, &'c str> {
    v
}

fn main() {}
PhantomData<&'a mut T>
use std::marker::PhantomData;
struct MyIter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
    // 注意这里 T 实际是不可能这样写的, 因为关于结构体对T的协变规则编译器已经能得出了, 你是没有意图去这样做的
    // 相当于假装这里有个成员 , 它的类型是 &'a mut T
    // &mut T对T 是不可变的, 那么现在整个结构体对于T 从原来的协变 变成 不变了.
    // 这改变了 原本对T 的型变情况(*const T 对T 是协变的), 是完全没有必要的, 没有这个意图的.
    // 所以我们对这种情况使用 PhantomData<&'a ()> 就可以了.
    _marker: PhantomData<&'a mut T>,
}
// 编译ok ,'a 是协变的
fn test1<'a: 'b, 'b, T: 'a>(v: MyIter<'a, T>) -> MyIter<'b, T> {
    v
}
// 'a:'c , 'c:'a 说明需要'a='c, T 是不变的
fn test2<'a: 'c, 'b, 'c: 'a>(v: MyIter<'b, &'a str>) -> MyIter<'b, &'c str> {
    v
}
// 编译ok
// 参数推断出'a:'b , 返回值推断出'b:'c
// 因为我们需要  'a='b , 设置 'b:'a 就能编译通过.
// 这里我们设置 'c:'a  能得出 'a='b='c了, 也是ok的
// 对于T 是不变的
fn test3<'a, 'b, 'c: 'a>(v: MyIter<'b, &'a str>) -> MyIter<'c, &'b str> {
    v
}

fn main(){}
PhantomData<&'a T>
use std::marker::PhantomData;

struct MyIter<'a, T: 'a> {
    ptr: *mut T,
    end: *mut T,
    // 因为自定义类型 对T 的型变要看成员对T的型变
    // 所以这里 PhantomData<&'a T>,设置上对T 是协变的
    // 但是实际上 是 不变的, 因为 成员类型*mut T对T 是不变的
    _marker: PhantomData<&'a T>,
}
// 编译ok
fn test<'a: 'b, 'b, T: 'a>(v: MyIter<'a, T>) -> MyIter<'b, T> {
    v
}
// 编译错误, 因为MyIter 对于T 是不变的
fn test2<'a: 'b, 'b>(v: MyIter<'b, &'a str>) -> MyIter<'b, &'b str> {
    v
}
fn main(){}
Important
  • 使整个结构体对于 ’a 是逆变的
use std::marker::PhantomData;
struct MyStruct<'a> {
    data: i32,
    _marker: PhantomData<fn(&'a ())>,
}

fn test1<'a, 'b: 'a>(v: MyStruct<'a>) -> MyStruct<'b> {
    v
}
fn main() {}
Important
  • 使整个结构体对于 ’a 是不变的
use std::marker::PhantomData;
struct MyStruct<'a> {
    data: i32,
    _marker: PhantomData<std::cell::Cell<&'a ()>>,
}

fn test1<'a: 'b, 'b: 'a>(v: MyStruct<'a>) -> MyStruct<'b> {
    v
}
fn main() {}
Back to top