导读

本文是笔者对Common Rust lifetime misconceptions一文的理解。

作者使用Rust 4多年,整理了这份针对lifetime的常见误解以及纠正。误解如何产生呢?笔者想大概要归因于早期的文章和资料不够充分全面,许多人(比如笔者)会用其他编程语言的经验往rust上套,导致认识与Rust实际机制的偏差。包括笔者写的这篇文章,也可能让读者产生新的误解。

不管是萌新还是老手,原文都值得一读。说不定将来招Rust开发,面试官会拿里面的内容来提问呢 :-)

内容

泛型T

泛型参数T 代表了所有类型,包括所有非引用类型,不可变引用、可变引用类型。因此:

  1. impl<T> Trait for T{}
  2. impl<T> Trait for &T{} // 编译失败: 重复实现

T:’static

这一段重点讨论 static variable , T:'static , &'static T 三者的差异。
首先是静态变量,它的特点是:

  • 编译期分配空间,运行时不再修改。
  • 可变或者不可变。修改静态变量只能在unsafe代码中进行。
  • 整个程序运行期间都存活。

&'static T 说明对类型为T的对象obj的借用范围为 'static ,但这并不代表obj必须是静态变量。 Box::leak() 方法可以以内存泄漏为代价, 在运行时创建在程序运行期间一直存活的对象。

T:'static 表示 :

T is bounded by a ‘static lifetime.

这个表示很有意思,因为大多数人将 T:'static 理解为”T具有’static生命周期”。
所以它表示的含义为:

  • T可以内部带有 'static 引用的类型, 如 Vec<&'static i32>
  • 也可以不带引用的类型, 例如: i32 , Vec<i32> , Vec<String>

一个奇怪的地方:

  1. fn miao<T:'static + Debug>(d: T){
  2. println!("{:?}", d);
  3. }
  4. fn main(){
  5. miao(vec![1, 2, 3]); // ok
  6. miao(vec![&1, &2]); // 也ok,可能与编译器优化有关。
  7. let d = 10;
  8. miao(vec![&d]); // error, d不能产生'static借用
  9. }

&’a T与T:’a

这一节是对上一节 T:'static 问题的泛化。
'static 是最大的生命周期, 而’a表示所有可能的生命周期,它在编译时具体化

从父类子类的角度看,范围越大的生命周期约束是范围小的约束的子类。这就好比:

  • 我一天工作24小时, 那么10:00-18:00这段时间我在工作
  • 10:00-18:00这段时间我在工作, 但我不能说我一天工作24小时

T:'a 具体表示的含义为:

  • T可以带有引用,其引用的生命周期类型为 'a 的子类,也就是生命周期范围要不小于 'a 的范围。如 Vec<&'static i32> , Vec<&'a i32>
  • 也可以不带’a引用的类型, 例如: i32 , Vec<i32> , Vec<String>

通过编译不等于语义正确

不一定,rust编译器对注解的要求是内存安全即可。同时rust编译器会隐式标记,可能会导致与开发者想要的语义不一样。
另一方面,开发者错误标记注解,可能会导致过分严格的生命周期限制:

  1. struct S<'a>{
  2. data: &'a i32,
  3. }
  4. impl<'a> S<'a>{
  5. // 具体语义为:调用此方法产生的借用将持续到S被回收
  6. // 导致的结果是一旦这个方法被调用, 接受者将无法再次产生其他借用
  7. fn method(&'a mut self){}
  8. }

Trait Object也有lifetime constraint

这是因为大多数情况下,编译器自动推导了生命周期约束。

  1. type BoxA = Box<dyn Trait>;
  2. type BoxA = Box<dyn Trait + 'static>; // 如果trait不含生命周期参数,将推导为'static
  3. trait T<'a>{}
  4. type BoxB<'a> = Box<dyn T<'a>>;
  5. type BoxB<'a> = Box<dyn T<'a> + 'a>;

编译器的提示不一定符合开发者想要的语义

编译失败,编译器提出的解决办法可能过于保守:

  1. // 编译失败
  2. fn box<T:Display>(t:T) -> Box<dyn Display>{
  3. Box::new(t)
  4. }
  5. // 编译器推荐这样
  6. fn box<T:Display + 'static>(t:T) -> Box<dyn Display>{
  7. Box::new(t)
  8. }
  9. // 实际上开发者想要的可能是这样
  10. fn box<'a, T:Display + 'a>(t:T) -> Box<dyn Display + 'a>{
  11. Box::new(t)
  12. }

lifetime constraint是编译期属性

不能在运行时动态改变对象的lifetime constraint。如果对象内部有多个借用,那么会挑选其中最小的范围作为约束(如果有借用与其他借用没有交集,编译错误),一旦确定了这个lifetime就不会更改。

&mut T转为&T不一定安全

先介绍Rust的re-borrowing:

  • 允许对借用一个引用, 也即是 let b = & &a 。在reborrowing下,允许同一时间存在多个对原对象的可变借用,但是它们对原对象的访问不能重叠。

    1. fn main(){
    2. let mut a = 1;
    3. let mut b = &mut a;
    4. let c = &mut b; // re-borrowing, ok
    5. // compile error
    6. //let d = &mut a;
    7. // 如果交换下面两行就会编译错误
    8. println!("{}", c); // compiler到这里可以回收c
    9. println!("{}", b);
    10. }

    这一节主要讨论一个误解:re-borrowing一个引用会立刻回收它,产生另一个引用。但实际上:

    1. let mut a = 10;
    2. let b = &*(&mut a); // 看起来&mut a会被立刻回收,但实际上它持续到最后一次使用b, 也就是第5行
    3. let c = &a; // 编译失败: 借用冲突了, 可改为 let c = &b;
    4. println!("{}", c);
    5. println!("{}", b);

闭包中的生命周期与函数不太一样

目前的Rust中,生命周期机制在闭包和函数中有差异。这部分建议阅读原文

&’static str不一定会转为&’a str

作者给了个例子,这个例子也与闭包相关:

  1. use rand;
  2. fn generic_str_fn<'a>() -> &'a str {"str"}
  3. fn static_str_fn() -> &'static str {"str"}
  4. fn a_or_b_fn<T, F>(a: T, b_fn: F) -> T
  5. where F: Fn() -> T
  6. {
  7. if rand::random() {
  8. a
  9. } else {
  10. b_fn()
  11. }
  12. }
  13. fn main() {
  14. let some_string = "string".to_owned();
  15. let some_str = &some_string[..];
  16. let str_ref = a_or_b_fn(some_str, generic_str_fn); // compiles
  17. let str_ref = a_or_b_fn(some_str, static_str_fn); // compile error
  18. }

直觉告诉我们,编译器要进行生命周期约束的转换: &'static str 可以转为 &'a str ;但实际上,编译器报错了,因为它打算对str_ref 产生 &'static str 借用,这是不行的。

参考