10|生命周期:你创建的值究竟能活多久?

182 阅读6分钟

正式开始

在 Rust 中,除非显式地做 Box::leak() / Box::into_raw() / ManualDrop 等动作,一般来说,堆内存的生命周期,会默认和其栈内存的生命周期绑定在一起

值的生命周期

  1. 静态生命周期:一个值的生命周期贯穿整个进程的生命周期

    a. 当值拥有静态生命周期,其引用也具有静态生命周期,可以用 'static 来表示

    b. &'static str 代表这是一个具有静态生命周期的字符串不可变借用

    c. 一般来说,全局变量、静态变量、字符串字面量(string literal)等,都拥有静态生命周期

  2. 动态生命周期:如果一个值是在某个作用域中定义的,也就是说它被创建在栈上或者堆上,那么其生命周期是动态的

    a. 当这个值的作用域结束时,值的生命周期也随之结束

    b. 对于动态生命周期,我们约定用 'a 、'b 或者 'hello 这样的小写字符或者字符串来表述。 ' 后面具体是什么名字不重要,它代表某一段动态的生命周期

小结

image.png

  1. 分配在堆和栈上的内存有其各自的作用域,它们的生命周期是动态的。
  2. 全局变量、静态变量、字符串字面量、代码等内容,在编译时会被编译到可执行文件的 BSS/Data/RoData/Text 段,然后在加载时装入内存。它们的生命周期和进程的生命周期一致,是静态的。
  3. 函数指针的生命周期也是静态的,因为函数在 Text 段中,只要进程活着,其内存一直存在

编译器如何识别生命周期

生命周期标注的目的是,在参数和返回值之间建立联系或者约束。调用函数时,传入的参数的生命周期需要大于等于(outlive)标注的生命周期

image.png

  1. 函数本身携带的信息,就是编译器在编译时使用的全部信息
  2. 需要我们在函数签名中提供生命周期的信息,也就是生命周期标注(lifetime specifier)
  3. 在生命周期标注时,使用的参数叫生命周期参数(lifetime parameter)
  4. 通过生命周期标注,我们告诉编译器这些引用间生命周期的约束
  5. 生命周期参数的描述方式和泛型参数一致,不过只使用小写字母
  6. 生命周期参数,描述的是参数和参数之间、参数和返回值之间的关系,并不改变原有的生命周期

编译器自动添加生命周期的规则

所有使用了引用的函数,都需要生命周期的标注

  1. 所有引用类型的参数都有独立的生命周期 'a 、'b 等。
  2. 如果只有一个引用型输入,它的生命周期会赋给所有输出。
  3. 如果有多个引用类型的参数,其中一个是 self,那么它的生命周期会赋给所有输出。

image.png

编译器如何检查

  1. 当每个函数都添加好生命周期标注后,编译器就可以从函数调用的上下文中分析出,在传参时引用的生命周期是否和函数签名中要求的生命周期匹配。
  2. 如果不匹配,就违背了“引用的生命周期不能超出值的生命周期”,编译器就会报错

小结

  1. 根据所有权规则,值的生命周期可以确认,它可以一直存活到所有者离开作用域

  2. 引用的生命周期不能超过值的生命周期。在同一个作用域下,这是显而易见的。然而,当发生函数调用时,编译器需要通过函数的签名来确定,参数和返回值之间生命周期的约束

    a. 大多数情况下,编译器可以通过上下文中的规则,自动添加生命周期的约束

    b. 如果无法自动添加,则需要开发者手工来添加约束

     1. 一般,我们只需要确定好返回值和哪个参数的生命周期相关就可以了
     2. 对于数据结构,当内部有引用时,我们需要为引用标注生命周期
    

好的链接

  1. The Rust Programming Language 生命周期
  2. 死灵书中的生命周期

精选问答

  1. 对&str这个类型的理解?

    a. &str是一个字符串切片,一个带有长度的胖指针,指向字符串实际的位置

    b. 它可以指向 "hello world",此时指针指到了 RODATA/STRING section 里 "hello" 的地址,它的生命周期是 'static

    c. 也可以指向 "hello world".to_string(),此时指针指向了这个字符串的堆地址,生命周期是 'a。

  2. 对生命周期检查的一点思考

    a. 在含有引用的函数调用中,编译器会尝试根据规则进行补齐,如果无法自动补齐,就会要求开发者进行标注

    b. 开发者标注的生命周期会在两个地方生效

     1. 函数的实现中,会去校验标注的正确性
     2. 在函数的调用点也会根据函数声明中的标注,对入参和返回值进行检查
    

    c. 函数声明中的生命周期标注,其实就是同时约束实现方和调用方的约定。在标注的约束关系中,如果检查发现调用方和实现方都满足约束,则编译通过

  3. ”&mut &str 添加生命周期后变成 &'b mut &'a str“,为什么编译器会自动标注成这样?不是一个参数一个生命周期吗?

    a. 一个引用一个生命周期

  4. 某个函数的参数生命周期是变化的,可能是<'a,'a>也可能是<'a,'b>,这样的话函数要实现2遍吗

    a. 'a 代表的是一个泛化的生命周期,表示我支持任何长度的生命周期

    比如 fn max<'a, 'b: 'a>(s1: &'a str, s2: &'b str) -> &'a str {} b. 表明 'a 和 'b 可能是不同的生命周期,'b 生命周期大于等于 'a

    c. 返回值需要满足 'a 的生命周期。整个函数相当于在说,给定两个参数,返回值的生命周期要大于这两个参数中小的那个

    d. 所以函数参数的生命周期是一种约束,而不是一个具体的值。它本身就可以随着传入的生命周期而自适应,只要它们满足约束就可以