原文:https://github.com/pretzelhammer/rust-blog/blob/master/posts/common-rust-lifetime-misconceptions.md
译者的话:从我开始学习Rust到现在已经4年了,我已经敢于自称Rust熟练工了,而我自始至终感到困难的概念只有一个:lifetime。一方面是理解上的困难,另一方面是实际使用时,如果需要标注lifetime,或者编译器报了lifetime相关的错,总会让我十分烦燥。我相信有这样体验的人并不少。这篇文章是我读过的最好的lifetime释疑的文章,其中的大部分内容我其实已经有了模糊的理解,但并不都能清晰地表述出来;但对于哪怕是1年前的我,如果能读到这篇文章,将会使我少走很多弯路。对于Rust初学者,毫不夸张地说,你可以不读官方的book另外选本书入门,而这篇文章必读;对于理解了lifetime的老手,这篇文章也是非常好的reference。更推荐大家去读原文,翻译它主要是为了加深自己的理解。

引言

我曾经有过所有这些误解,而我看到现在很多初学者仍然被它们困扰。我的某些术语可能不是标准说法,下面的表格是我将使用的缩略语和对应的含义。

缩略语 含义
T
1. 含有所有类型的集合或
1. 此集合中的某个类型
独立类型 非引用的类型,例如 i32、String、Vec 等

1. 借用类型 或
1. 引用类型
某个不关注可变性的引用类型,例如 &i32、&mut i32 等

1. 可变引用 或
1. 互斥引用
互斥可变引用,即 &mut T

1. 不可变引用 或
1. 共享引用
共享不可变引用,即 &T

误解

一言以蔽之:一个变量的 lifetime 是编译器能静态地确定它指向的数据在它当前的地址有效的区间。我将会使用接下来的6500字解释人们常常困惑的细节。

1) T只包括独立类型

这个误解更多是关于泛型而不是lifetime的,但是Rust里的泛型和lifetime是紧紧纠缠在一起的,不可能只讨论其中一个而不涉及到另一个。

总之:当我最初学习Rust时,我理解 i32&i32&mut i32 是不同的类型。

我也理解泛型类型参数 T 代表了一个包括所有类型的集合。

然而,虽然我分别理解这两者,我并不能把它们合起来。

在我初学者的Rust思维里,我是这么理解泛型的行为的:

类型变量 T &T &mut T
例子 i32 &i32 &mut i32
  • T 包括所有独立类型。
  • &T 包括所有不可变借用类型。
  • &mut T 包括所有可变借用类型。
  • T&T&mut T 是不相交的有限集。

美妙,简单,清晰,容易,符合直觉,并且完全错误

这才是Rust里泛型真正的行为:

类型变量 T &T &mut T
例子 i32 , &i32 , &mut i32 , &&i32 , &mut &mut i32 ,.. &i32 , &&i32 , &&mut i32 ,… &mut i32 , &mut &mut i32, &mut &i32,…
  • T&T&mut T 都是无限集,因为可以无限次地借用一个类型。
  • T&T&mut T 的超集。
  • &T&mut T 是不相交的集合。

下面是几个验证这些概念的例子:

  1. trait Trait {}
  2. impl<T> Trait for T {}
  3. impl<T> Trait for &T {} // compile error
  4. impl<T> Trait for &mut T {} // compile error

上面的程序编译失败:

  1. error[E0119]: conflicting implementations of trait `Trait` for type `&_`:
  2. --> src/lib.rs:5:1
  3. |
  4. 3 | impl<T> Trait for T {}
  5. | ------------------- first implementation here
  6. 4 |
  7. 5 | impl<T> Trait for &T {}
  8. | ^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&_`
  9. error[E0119]: conflicting implementations of trait `Trait` for type `&mut _`:
  10. --> src/lib.rs:7:1
  11. |
  12. 3 | impl<T> Trait for T {}
  13. | ------------------- first implementation here
  14. ...
  15. 7 | impl<T> Trait for &mut T {}
  16. | ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&mut _`

编译器并不允许我们为 &T&mut T 实现 Trait ,因为这会和 TTrait 实现相冲突,它就已经包括了所有 &T&mut T

而如下的程序编译成功,因为 &T&mut T 是不相交的:

  1. trait Trait {}
  2. impl<T> Trait for &T {} // compiles
  3. impl<T> Trait for &mut T {} // compiles

要点:
T&T&mut T 的超集
&T&mut T 是不相交的集合

2) 如果T: ‘static,那么T必须在整个程序运行时都有效

误解的推论

  • T : 'static 应该读做 T'static lifetime
  • &'static TT: 'static 是同一个东西
  • 如果 T: 'static 那么 T 必须是不可变的
  • 如果 T: 'static 那么 T 只能在编译时创建

大部分Rust初学者第一次接触到 'static lifetime,都是在像这样的代码里:

  1. fn main() {
  2. let str_literal: &'static str = "str literal";
  3. }

他们学到, "str literal" 是被硬编码在编译出的二进制文件里的,在运行时被加载到只读内存中。
因此,它是不可变的,并且在整个程序运行时有效,因此它是 'static 的。
这些概念在后续使用 static 关键词定义静态变量时被进一步强化了。

  1. // Note: This example is purely for illustrative purposes.
  2. // Never use `static mut`. It's a footgun. There are
  3. // safe patterns for global mutable singletons in Rust but
  4. // those are outside the scope of this article.
  5. static BYTES: [u8; 3] = [1, 2, 3];
  6. static mut MUT_BYTES: [u8; 3] = [1, 2, 3];
  7. fn main() {
  8. MUT_BYTES[0] = 99; // compile error, mutating static is unsafe
  9. unsafe {
  10. MUT_BYTES[0] = 99;
  11. assert_eq!(99, MUT_BYTES[0]);
  12. }
  13. }

关于静态变量

  • 它们只能在编译时创建
  • 它们应该是不可变的,改变它们是 unsafe 的
  • 它们在整个程序运行时都有效

'static lifetime 大概是得名于静态变量的默认 lifetime,对吧?
所以 'static lifetime 也要遵循所有同样的规则,对吧?

并不是不对,但是一个有 'static lifetime 的类型,和一个被 'static lifetime 约束的类型是不同的。
后者能在运行时创建,能被安全和随意地改变,能被 drop,能活任意时长。

区分 &'static TT: 'static 是很重要的。

&'static T 是一个指向某个 T 的不可变引用,它能够被安全地持有无限久,直到整个程序终止。仅当 T 本身不可变,且在引用创建后就不再 move 才能满足这些条件。T 不需要在编译时创建。在运行时生成动态分配的随机数据,然后返回一个 'static 引用是可行的,只需要付出内存泄漏的代价,例如

  1. use rand;
  2. // generate random 'static str refs at run-time
  3. fn rand_str_generator() -> &'static str {
  4. let rand_string = rand::random::<u64>().to_string();
  5. Box::leak(rand_string.into_boxed_str())
  6. }

T: 'static 是一个能够被安全地持有无限久的 T,直到整个程序终止。
T: 'static 包括了所有的 &'static T,以及所有的独立类型,例如 StringVec 等等。
数据的拥有者得到保证,只要它还持有数据,数据就不会失效。
因此拥有者可以无限久地持有数据,直到整个程序终止。
T: 'static 应该读作 T'static lifetime 约束” ,而不是 T'static lifetime

一段阐明这些概念的程序:

  1. use rand;
  2. fn drop_static<T: 'static>(t: T) {
  3. std::mem::drop(t);
  4. }
  5. fn main() {
  6. let mut strings: Vec<String> = Vec::new();
  7. for _ in 0..10 {
  8. if rand::random() {
  9. // all the strings are randomly generated
  10. // and dynamically allocated at run-time
  11. let string = rand::random::<u64>().to_string();
  12. strings.push(string);
  13. }
  14. }
  15. // strings are owned types so they're bounded by 'static
  16. for mut string in strings {
  17. // all the strings are mutable
  18. string.push_str("a mutation");
  19. // all the strings are droppable
  20. drop_static(string); // compiles
  21. }
  22. // all the strings have been invalidated before the end of the program
  23. println!("i am the end of the program");
  24. }

要点:

  • T: 'static 应该读作 T'static lifetime约束”
  • 如果 T: 'static,那么 T 可以是一个有 'static lifetime 的借用类型,或者是一个独立类型
  • T: 'static 包括了所有独立类型,也就意味着 T
    • 可以在运行时动态创建
    • 不需要在整个程序运行时
    • 可以被安全和随意地改变
    • 可以在运行时动态地drop
    • 可以拥有任意的lifetime

      3) &’a T和T: ‘a是同一个东西

      这个误解是上一个的泛化版本。

&'a T 要求也意味着 T: 'a,因为如果 T 本身不在整个 'a 有效, T 的的引用不可能在整个 'a 都有效。

例如,Rust编译器不会允许构造类型 &'static Ref<'a, T>,因为如果 Ref 仅在 'a 有效,我们不可能构造一个它的 'static 引用。

T: 'a 包括了所有 &'a T,但反过来是不成立的。

  1. // only takes ref types bounded by 'a
  2. fn t_ref<'a, T: 'a>(t: &'a T) {}
  3. // takes any types bounded by 'a
  4. fn t_bound<'a, T: 'a>(t: T) {}
  5. // owned type which contains a reference
  6. struct Ref<'a, T: 'a>(&'a T);
  7. fn main() {
  8. let string = String::from("string");
  9. t_bound(&string); // compiles
  10. t_bound(Ref(&string)); // compiles
  11. t_bound(&Ref(&string)); // compiles
  12. t_ref(&string); // compiles
  13. t_ref(Ref(&string)); // compile error, expected ref, found struct
  14. t_ref(&Ref(&string)); // compiles
  15. // string var is bounded by 'static which is bounded by 'a
  16. t_bound(string); // compiles
  17. }

要点:

  • T: 'a&'a T 更通用和灵活
  • T: 'a 接受独立类型,含有引用的独立类型,以及
  • &'a T 只接受引用
  • 如果 T: 'static,那么 T: 'a,因为任意 'a 均满足 'static >= 'a
    (译注:我理解就 lifetime 而言不可变引用 &'a T 其实是和 Ref<'a, T> 同等的地位,甚至可以说前者是后者的一种。只不过前者非常常见,而后者绝大部分情况下追根溯源来自前者)

    4)我的代码不是泛型化的,也没有lifetime

误解的推论:

  • 可以避免使用泛型和 lifetime

这个令人安心的误解来源于Rust的 lifetime 省略规则,即 borrow checker 会根据如下规则推导函数的 lifetime 标注,从而免去你手写的必要:

  • 每一个函数的输入引用会被分配一个单独的 lifetime
  • 如果只有一个输入 lifetime,它被应用给所有输出引用
  • 如果有多个输入 lifetime,但其中一个是 &self 或者 &mut self,那么 self 的 lifetime 被应用给所有输出引用
  • 否则,输出 lifetime 必须显式标注

这么多的规则,所以让我们看一些例子:

生命周期省略的三个规则

  • 编译器使用 3 个规则在没有显示标注生命周期的情况下,来确定引用的生命
    • 规则 1 应用于输入生命周期
    • 规则 2、3 应用于输出生命周期
    • 如果编译器应用完 3 个规则之后,仍然有无法确定生命周期的引用 -> 报错
    • 这些规则适用于 fn 定义和 impl 块
  • 规则 1:每个引用类型的参数都有自己的生命周期
  • 规则 2:如果只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
  • 规则 3:如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self(是方法),那么 self 的生命周期会被赋给所有的输出生命周期参数
  1. // elided
  2. fn print(s: &str);
  3. // expanded
  4. fn print<'a>(s: &'a str);
  5. // elided
  6. fn trim(s: &str) -> &str;
  7. // expanded
  8. fn trim<'a>(s: &'a str) -> &'a str;
  9. // illegal, can't determine output lifetime, no inputs
  10. fn get_str() -> &str;
  11. // explicit options include
  12. fn get_str<'a>() -> &'a str; // generic version
  13. fn get_str() -> &'static str; // 'static version
  14. // illegal, can't determine output lifetime, multiple inputs
  15. fn overlap(s: &str, t: &str) -> &str;
  16. // explicit (but still partially elided) options include
  17. fn overlap<'a>(s: &'a str, t: &str) -> &'a str; // output can't outlive s
  18. fn overlap<'a>(s: &str, t: &'a str) -> &'a str; // output can't outlive t
  19. fn overlap<'a>(s: &'a str, t: &'a str) -> &'a str; // output can't outlive s & t
  20. fn overlap(s: &str, t: &str) -> &'static str; // output can outlive s & t
  21. fn overlap<'a>(s: &str, t: &str) -> &'a str; // no relationship between input & output lifetimes
  22. // expanded
  23. fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'a str;
  24. fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'b str;
  25. fn overlap<'a>(s: &'a str, t: &'a str) -> &'a str;
  26. fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'static str;
  27. fn overlap<'a, 'b, 'c>(s: &'a str, t: &'b str) -> &'c str;
  28. // elided
  29. fn compare(&self, s: &str) -> &str;
  30. // expanded
  31. fn compare<'a, 'b>(&'a self, &'b str) -> &'a str;

只要你写了:

  • 结构体方法(译注:这里应该只是指 &self&mut self 方法)
  • 以引用为参数的函数
  • 返回引用的函数
  • 泛型函数
  • trait object(后面详述)
  • 闭包(后面详述)

那么你的代码就充满了泛型省略 lifetime 标注。

要点

  • 几乎所有 Rust 代码都是泛型代码,到处都是省略的 lifetime 标注

    5) 如果通过了编译,那么我的lifetime标注就是正确的

    误解的推论

  • Rust 的函数的 lifetime 省略规则总是正确的

  • Rust 的 borrow checke 总是正确的,无论是技术上还是语义上(译注:技术上是指内存安全,参见后面;语义上是指程序的意图,例如函数返回的引用是来自于哪一个输入参数)
  • Rust 比我更懂我的程序的语义

当一个 Rust 程序技术上可通过编译,它在语义上仍然可能是错的。例如:

  1. struct ByteIter<'a> {
  2. remainder: &'a [u8]
  3. }
  4. impl<'a> ByteIter<'a> {
  5. fn next(&mut self) -> Option<&u8> {
  6. if self.remainder.is_empty() {
  7. None
  8. } else {
  9. let byte = &self.remainder[0];
  10. self.remainder = &self.remainder[1..];
  11. Some(byte)
  12. }
  13. }
  14. }
  15. fn main() {
  16. let mut bytes = ByteIter { remainder: b"1" };
  17. assert_eq!(Some(&b'1'), bytes.next());
  18. assert_eq!(None, bytes.next());
  19. }

ByteIter 是一个在 byte 的切片上迭代的迭代器。为了简洁,这里并没有实现 Iterator trait。

它看起来正常工作,但是如果我们想同时检查多个 byte 呢?

  1. fn main() {
  2. let mut bytes = ByteIter { remainder: b"1123" };
  3. let byte_1 = bytes.next();
  4. let byte_2 = bytes.next();
  5. if byte_1 == byte_2 {
  6. // do something
  7. }
  8. }

编译错误:

  1. error[E0499]: cannot borrow `bytes` as mutable more than once at a time
  2. --> src/main.rs:20:18
  3. |
  4. 19 | let byte_1 = bytes.next();
  5. | ----- first mutable borrow occurs here
  6. 20 | let byte_2 = bytes.next();
  7. | ^^^^^ second mutable borrow occurs here
  8. 21 | if byte_1 == byte_2 {
  9. | ------ first borrow later used here

我想我们可以改成 copy 每个 byte。当我们在处理 byte 的时候,这样做是没问题的。

但是如果我们把 ByteIter 扩展为一个能够迭代任意的& 'a [T] 的通用切片迭代器时,
我们将来可能会想要用它处理 copy/clone 会很耗费甚至不可能的类型。

好吧,看来我们无计可施了,代码通过编译了,所以 lifetime 标注肯定是正确的,对吧?

并不,现在的 lifetime 标注正是 bug 的原因!错误的 lifetime 标注被省略了,所以想看出这一点并不容易。让我们补上被省略的 lifetime,以获得更清晰的视野:

  1. struct ByteIter<'a> {
  2. remainder: &'a [u8]
  3. }
  4. impl<'a> ByteIter<'a> {
  5. fn next<'b>(&'b mut self) -> Option<&'b u8> {
  6. if self.remainder.is_empty() {
  7. None
  8. } else {
  9. let byte = &self.remainder[0];
  10. self.remainder = &self.remainder[1..];
  11. Some(byte)
  12. }
  13. }
  14. }

然而这并没有什么用。我还是没看懂。

这里有一条只有Rust专家才知道的小诀窍:给 lifetime 一个有意义的名字。让我们再试试:

  1. struct ByteIter<'remainder> {
  2. remainder: &'remainder [u8]
  3. }
  4. impl<'remainder> ByteIter<'remainder> {
  5. fn next<'mut_self>(&'mut_self mut self) -> Option<&'mut_self u8> {
  6. if self.remainder.is_empty() {
  7. None
  8. } else {
  9. let byte = &self.remainder[0];
  10. self.remainder = &self.remainder[1..];
  11. Some(byte)
  12. }
  13. }
  14. }

返回的byte被标注为 'mut_self,但显然它们是来自于 'remainder! 试试修复它。

  1. struct ByteIter<'remainder> {
  2. remainder: &'remainder [u8]
  3. }
  4. impl<'remainder> ByteIter<'remainder> {
  5. fn next(&mut self) -> Option<&'remainder u8> {
  6. if self.remainder.is_empty() {
  7. None
  8. } else {
  9. let byte = &self.remainder[0];
  10. self.remainder = &self.remainder[1..];
  11. Some(byte)
  12. }
  13. }
  14. }
  15. fn main() {
  16. let mut bytes = ByteIter { remainder: b"1123" };
  17. let byte_1 = bytes.next();
  18. let byte_2 = bytes.next();
  19. std::mem::drop(bytes); // we can even drop the iterator now!
  20. if byte_1 == byte_2 { // compiles
  21. // do something
  22. }
  23. }

现在我们再回头去看看我们程序的上一个版本,它显然是错的。

那为什么编译通过了?答案很简单:它是内存安全的。

对于 lifetime 标注,Rust 的 borrow checker 只在乎能否用它静态地验证程序的内存安全。

即使 lifetime 标注有语义上的错误,Rust 也会愉快地成功编译,而后果就是程序变得不必要的严格。

下面是一个上面例子的反面:Rust 的 lifetime 省略规则碰巧在语义上是正确的,但是我们无意识地用不必要的 lifetime 标注加上了过于严格的限制。

  1. #[derive(Debug)]
  2. struct NumRef<'a>(&'a i32);
  3. impl<'a> NumRef<'a> {
  4. // my struct is generic over 'a so that means I need to annotate
  5. // my self parameters with 'a too, right? (answer: no, not right)
  6. fn some_method(&'a mut self) {}
  7. }
  8. fn main() {
  9. let mut num_ref = NumRef(&5);
  10. num_ref.some_method(); // mutably borrows num_ref for the rest of its lifetime
  11. num_ref.some_method(); // compile error
  12. println!("{:?}", num_ref); // also compile error
  13. }

当我们有一个关于 'a 泛型的结构体时,我们几乎不会需要写带有 &'a mut self 的方法。

它的含义是 “在这个结构体的整个 lifetime,这个方法会可变借用它”。

结果就是 Rust 的 borrow checker 只会允许最多一次对此方法的调用,然后结构体就被永久地可变借用了,无法用于其它用途。

需要这样做的情形非常少,但对于一个懵懂的初学者,他很容易这样写,而且编译还通过了。

修复方法是不要加上不必要的显式 lifetime 标注,让 Rust 的 lifetime 省略规则处理它:

  1. #[derive(Debug)]
  2. struct NumRef<'a>(&'a i32);
  3. impl<'a> NumRef<'a> {
  4. // no more 'a on mut self
  5. fn some_method(&mut self) {}
  6. // above line desugars to
  7. fn some_method_desugared<'b>(&'b mut self){}
  8. }
  9. fn main() {
  10. let mut num_ref = NumRef(&5);
  11. num_ref.some_method();
  12. num_ref.some_method(); // compiles
  13. println!("{:?}", num_ref); // compiles
  14. }

要点

  • Rust 的函数的 lifetime 省略规则并不是所有情形都总是正确的
  • Rust 并不比你更懂你程序的语义
  • 给你的 lifetime 标注有意义的名字
  • 当添加显式 lifetime 标注时,想清楚加在哪和为什么

    6) boxed trait object没有lifetime

    前面我们讨论了 Rust 的函数的 lifetime 省略规则。对于 trait object,也有省略规则:

  • 如果一个 trait object 被用于泛型类型的类型参数,则它的 lifetime 约束由容器类型推导(译注:容器就是指泛型类型例如Box、&T)

    • 如果容器类型只有一个 lifetime 约束,就用
    • 如果多于一个,必须显式指定一个 lifetime
  • 如果上面并不成立
    • 如果 trait 只有一个 lifetime 约束,就用
    • 如果任意一个 lifetime 约束使用了 'static,则用 'static
    • 如果 trait 没有 lifetime 约束,那么在表达式里它的 lifetime 是(根据上下文)推导的,否则是 'static

这些听上去非常复杂,但简单总结就是 “trait object 的 lifetime 约束是从上下文推导的”

通过下面这些例子,我们会看到 lifetime 约束推导非常符合直觉,所以我们并不需要记住正式的规则:

  1. use std::cell::Ref;
  2. trait Trait {}
  3. // elided
  4. type T1 = Box<dyn Trait>;
  5. // expanded, Box<T> has no lifetime bound on T, so inferred as 'static
  6. type T2 = Box<dyn Trait + 'static>;
  7. // elided
  8. impl dyn Trait {}
  9. // expanded
  10. impl dyn Trait + 'static {}
  11. // elided
  12. type T3<'a> = &'a dyn Trait;
  13. // expanded, &'a T requires T: 'a, so inferred as 'a
  14. type T4<'a> = &'a (dyn Trait + 'a);
  15. // elided
  16. type T5<'a> = Ref<'a, dyn Trait>;
  17. // expanded, Ref<'a, T> requires T: 'a, so inferred as 'a
  18. type T6<'a> = Ref<'a, dyn Trait + 'a>;
  19. trait GenericTrait<'a>: 'a {}
  20. // elided
  21. type T7<'a> = Box<dyn GenericTrait<'a>>;
  22. // expanded
  23. type T8<'a> = Box<dyn GenericTrait<'a> + 'a>;
  24. // elided
  25. impl<'a> dyn GenericTrait<'a> {}
  26. // expanded
  27. impl<'a> dyn GenericTrait<'a> + 'a {}

实现了 trait 的具体类型可以含有引用,因此它们也有 lifetime 约束,于是它们对应的 trait object 也有 lifetime 约束。

另外你可以直接为引用实现 trait,而引用显然是有 lifetime 约束的:

  1. trait Trait {}
  2. struct Struct {}
  3. struct Ref<'a, T>(&'a T);
  4. impl Trait for Struct {}
  5. impl Trait for &Struct {} // impl Trait directly on a ref type
  6. impl<'a, T> Trait for Ref<'a, T> {} // impl Trait on a type containing refs

无论如何,这很值得详细解释,因为当初学者把函数由使用 trait object 重构为使用泛型(或反过来时),常常会感到困惑。例如:

  1. use std::fmt::Display;
  2. fn dynamic_thread_print(t: Box<dyn Display + Send>) {
  3. std::thread::spawn(move || {
  4. println!("{}", t);
  5. }).join();
  6. }
  7. fn static_thread_print<T: Display + Send>(t: T) {
  8. std::thread::spawn(move || {
  9. println!("{}", t);
  10. }).join();
  11. }

它抛出如下编译错误:

  1. error[E0310]: the parameter type `T` may not live long enough
  2. --> src/lib.rs:10:5
  3. |
  4. 9 | fn static_thread_print<T: Display + Send>(t: T) {
  5. | -- help: consider adding an explicit lifetime bound...: `T: 'static +`
  6. 10 | std::thread::spawn(move || {
  7. | ^^^^^^^^^^^^^^^^^^
  8. |
  9. note: ...so that the type `[closure@src/lib.rs:10:24: 12:6 t:T]` will meet its required lifetime bounds
  10. --> src/lib.rs:10:5
  11. |
  12. 10 | std::thread::spawn(move || {
  13. | ^^^^^^^^^^^^^^^^^^

好的这很好,编译器告诉了我们如何修复问题,让我们试一试。

  1. use std::fmt::Display;
  2. fn dynamic_thread_print(t: Box<dyn Display + Send>) {
  3. std::thread::spawn(move || {
  4. println!("{}", t);
  5. }).join();
  6. }
  7. fn static_thread_print<T: Display + Send + 'static>(t: T) {
  8. std::thread::spawn(move || {
  9. println!("{}", t);
  10. }).join();
  11. }

现在编译通过了,但这两个函数放一起看起来很奇怪。

为什么第二个函数需要对 T'static 约束,而第一个函数不需要呢?

这是一个微妙的问题。

Rust 使用 lifetime 省略规则,自动地对第一个函数推导出了 'static 约束,所以实际上两个函数都是 'static 约束。Rust 编译器看到的实际是这样:

  1. use std::fmt::Display;
  2. fn dynamic_thread_print(t: Box<dyn Display + Send + 'static>) {
  3. std::thread::spawn(move || {
  4. println!("{}", t);
  5. }).join();
  6. }
  7. fn static_thread_print<T: Display + Send + 'static>(t: T) {
  8. std::thread::spawn(move || {
  9. println!("{}", t);
  10. }).join();
  11. }

要点:

  • 所有的 trait object 都有推导出来的默认 lifetime 约束

    7) 编译错误消息会告诉我如何修复我的程序

    **
    误解的推论

  • Rust 的 trait object 的 lifetime 省略规则总是正确的

  • Rust 比我更懂我的程序的语义

这个误解,就是前面两个误解合成一个例子:

  1. use std::fmt::Display;
  2. fn box_displayable<T: Display>(t: T) -> Box<dyn Display> {
  3. Box::new(t)
  4. }

抛出了如下错误:

  1. error[E0310]: the parameter type `T` may not live long enough
  2. --> src/lib.rs:4:5
  3. |
  4. 3 | fn box_displayable<T: Display>(t: T) -> Box<dyn Display> {
  5. | -- help: consider adding an explicit lifetime bound...: `T: 'static +`
  6. 4 | Box::new(t)
  7. | ^^^^^^^^^^^
  8. |
  9. note: ...so that the type `T` will meet its required lifetime bounds
  10. --> src/lib.rs:4:5
  11. |
  12. 4 | Box::new(t)
  13. | ^^^^^^^^^^^

行,让我们按编译器的建议修复这个问题,而不在意它自动地为我们的 boxed trait object 推导了 'static lifetime 约束,没有告诉我们,并且它建议的修复是基于这个隐含的事实:

  1. use std::fmt::Display;
  2. fn box_displayable<T: Display + 'static>(t: T) -> Box<dyn Display> {
  3. Box::new(t)
  4. }

现在程序编译通过了;但这真的是我们想要的吗?有可能,但也可能不是。编译器并没有提到任何其它的修复方式,但这也是正确的修复:

  1. use std::fmt::Display;
  2. fn box_displayable<'a, T: Display + 'a>(t: T) -> Box<dyn Display + 'a> {
  3. Box::new(t)
  4. }

这个函数接受所有之前的版本接受的参数,再加上更多!这是不是一个更好的函数?并不一定,这要看我们的程序的需求和限制。这个例子有一点抽象,让我们来看一个更简单和明显的情形:

  1. fn return_first(a: &str, b: &str) -> &str {
  2. a
  3. }

抛出错误:

  1. error[E0106]: missing lifetime specifier
  2. --> src/lib.rs:1:38
  3. |
  4. 1 | fn return_first(a: &str, b: &str) -> &str {
  5. | ---- ---- ^ expected named lifetime parameter
  6. |
  7. = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
  8. help: consider introducing a named lifetime parameter
  9. |
  10. 1 | fn return_first<'a>(a: &'a str, b: &'a str) -> &'a str {
  11. | ^^^^ ^^^^^^^ ^^^^^^^ ^^^

错误消息建议把输入和输出都标成同一个lifetime。如果我们这样做了,我们的程序能够编译,但这个函数会过于限制返回类型。我们真正想要的是这样:

  1. fn return_first<'a>(a: &'a str, b: &str) -> &'a str {
  2. a
  3. }

要点

  • Rust 的 trait object 的 lifetime 省略规则并不是所有情形都总是正确的
  • Rust 并不比你更懂你程序的语义
  • Rust 编译器的错误消息建议的修复方式,会让你的程序通过编译,但这和能让你的程序通过编译并且最符合你的程序的需求是两回事
    (译注:Rust 编译器的错误消息确实是独一无二的强大,但认为按其建议进行修改就能通过编译也过于狂热了。初学者写或者从别的语言翻译复杂程序的常见情况是,按编译器的建议改了一圈,最后编译器建议把它之前的建议再改回去,就怒而退坑了)

    8) lifetime能在运行时伸长缩短

    误解的推论

  • 容器类型能够在运行时交换引用,从而改变它们的lifetime

  • Rust的borrow checker进行了高级的控制流分析

如下代码无法编译:

  1. struct Has<'lifetime> {
  2. lifetime: &'lifetime str,
  3. }
  4. fn main() {
  5. let long = String::from("long");
  6. let mut has = Has { lifetime: &long };
  7. assert_eq!(has.lifetime, "long");
  8. {
  9. let short = String::from("short");
  10. // "switch" to short lifetime
  11. has.lifetime = &short;
  12. assert_eq!(has.lifetime, "short");
  13. // "switch back" to long lifetime (but not really)
  14. has.lifetime = &long;
  15. assert_eq!(has.lifetime, "long");
  16. // `short` dropped here
  17. }
  18. // compile error, `short` still "borrowed" after drop
  19. assert_eq!(has.lifetime, "long");
  20. }

错误消息:

  1. error[E0597]: `short` does not live long enough
  2. --> src/main.rs:11:24
  3. |
  4. 11 | has.lifetime = &short;
  5. | ^^^^^^ borrowed value does not live long enough
  6. ...
  7. 15 | }
  8. | - `short` dropped here while still borrowed
  9. 16 | assert_eq!(has.lifetime, "long");
  10. | --------------------------------- borrow later used here

这同样无法编译,抛出和上面完全一致的错误:

  1. struct Has<'lifetime> {
  2. lifetime: &'lifetime str,
  3. }
  4. fn main() {
  5. let long = String::from("long");
  6. let mut has = Has { lifetime: &long };
  7. assert_eq!(has.lifetime, "long");
  8. // this block will never run
  9. if false {
  10. let short = String::from("short");
  11. // "switch" to short lifetime
  12. has.lifetime = &short;
  13. assert_eq!(has.lifetime, "short");
  14. // "switch back" to long lifetime (but not really)
  15. has.lifetime = &long;
  16. assert_eq!(has.lifetime, "long");
  17. // `short` dropped here
  18. }
  19. // still a compile error, `short` still "borrowed" after drop
  20. assert_eq!(has.lifetime, "long");
  21. }

lifetime必须在编译时验证,Rust的borrow checker只会进行非常基本的控制流分析,它认为一个if-else语句的所有语句块和一个match语句的所有分支都可能走到,于是会为变量选择其中最短的lifetime。一但一个变量有了lifetime约束,它就永远被那个lifetime约束了。一个变量的lifetime只可能缩短,并且所有的缩短都发生在编译时。
要点

  • lifetime是在编译时静态验证的
  • 在运行时,lifetime不可能以任何方式伸长缩短或改变
  • Rust的borrow checker总是会假设所有的分支都会走到,为变量选择其中最短的lifetime

    9) 把可变引用降级为共享引用是安全的

    误解的推论

  • 重借用一个引用结束了它的lifetime,开启了一个新的lifetime

你能够把一个可变引用传给一个需要共享引用的函数,因为Rust会隐式地把可变引用重借用为不可变引用:

  1. fn takes_shared_ref(n: &i32) {}
  2. fn main() {
  3. let mut a = 10;
  4. takes_shared_ref(&mut a); // compiles
  5. takes_shared_ref(&*(&mut a)); // above line desugared
  6. }

直觉上这完全正确,因为把一个可变引用重借用为不可变没有任何危险,对吧?令人惊讶的是并非如此,如下的程序无法编译:

  1. fn main() {
  2. let mut a = 10;
  3. let b: &i32 = &*(&mut a); // re-borrowed as immutable
  4. let c: &i32 = &a;
  5. dbg!(b, c); // compile error
  6. }

抛出如下错误:

  1. error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
  2. --> src/main.rs:4:19
  3. |
  4. 3 | let b: &i32 = &*(&mut a);
  5. | -------- mutable borrow occurs here
  6. 4 | let c: &i32 = &a;
  7. | ^^ immutable borrow occurs here
  8. 5 | dbg!(b, c);
  9. | - mutable borrow later used here

一次可变借用确实发生了,但它被立即无条件地重借用为不可变,然后被drop掉。为什么Rust认为不可变的重借用仍然具有那个可变借用的互斥lifetime?虽然在上面这个例子里没有什么问题,但是允许把可变引用降级为共享引用确实会引入潜在的内存安全问题:

  1. use std::sync::Mutex;
  2. struct Struct {
  3. mutex: Mutex<String>
  4. }
  5. impl Struct {
  6. // downgrades mut self to shared str
  7. fn get_string(&mut self) -> &str {
  8. self.mutex.get_mut().unwrap()
  9. }
  10. fn mutate_string(&self) {
  11. // if Rust allowed downgrading mut refs to shared refs
  12. // then the following line would invalidate any shared
  13. // refs returned from the get_string method
  14. *self.mutex.lock().unwrap() = "surprise!".to_owned();
  15. }
  16. }
  17. fn main() {
  18. let mut s = Struct {
  19. mutex: Mutex::new("string".to_owned())
  20. };
  21. let str_ref = s.get_string(); // mut ref downgraded to shared ref
  22. s.mutate_string(); // str_ref invalidated, now a dangling pointer
  23. dbg!(str_ref); // compile error as expected
  24. }

这里的问题在于当你重借用一个可变引用为共享引用时,你得到了共享引用和一个巨大的“惊喜”:它延长了可变引用的lifetime,直到这个重借用结束,即使可变借用本身已经被drop。使用这个重借用的共享引用非常困难,因为它是不可变的却又不能和其它的共享引用重叠。这个重借用的共享引用具有可变引用的所有缺点和共享引用的所有缺点,却没有任意一个的优点。我认为重借用可变引用为共享引用是一个反模式。了解这个反模式是重要的,这样当你看到如下的代码时你就能轻易地识别出来:

  1. // downgrades mut T to shared T
  2. fn some_function<T>(some_arg: &mut T) -> &T;
  3. struct Struct;
  4. impl Struct {
  5. // downgrades mut self to shared self
  6. fn some_method(&mut self) -> &self;
  7. // downgrades mut self to shared T
  8. fn other_method(&mut self) -> &T;
  9. }

即使你在函数和方法的签名里避免了重借用,Rust仍然会自动地进行隐式的重借用,所以容易无意识地撞上去:

  1. use std::collections::HashMap;
  2. type PlayerID = i32;
  3. #[derive(Debug, Default)]
  4. struct Player {
  5. score: i32,
  6. }
  7. fn start_game(player_a: PlayerID, player_b: PlayerID, server: &mut HashMap<PlayerID, Player>) {
  8. // get players from server or create & insert new players if they don't yet exist
  9. let player_a: &Player = server.entry(player_a).or_default();
  10. let player_b: &Player = server.entry(player_b).or_default();
  11. // do something with players
  12. dbg!(player_a, player_b); // compile error
  13. }

上面的代码编译失败。or_default()返回了&mut Player,我们通过显式的类型标注把它隐式地重借用为&Player。我们必须这样写:

  1. use std::collections::HashMap;
  2. type PlayerID = i32;
  3. #[derive(Debug, Default)]
  4. struct Player {
  5. score: i32,
  6. }
  7. fn start_game(player_a: PlayerID, player_b: PlayerID, server: &mut HashMap<PlayerID, Player>) {
  8. // drop the returned mut Player refs since we can't use them together anyway
  9. server.entry(player_a).or_default();
  10. server.entry(player_b).or_default();
  11. // fetch the players again, getting them immutably this time, without any implicit re-borrows
  12. let player_a = server.get(&player_a);
  13. let player_b = server.get(&player_b);
  14. // do something with players
  15. dbg!(player_a, player_b); // compiles
  16. }

有一点笨拙,但这就是我们在内存安全的祭坛献上的祭品。
要点

  • 不要试图把可变引用重借用为共享引用,你不会开心的
  • 重借用一个可变引用不会终结它的lifetime,即使它被drop

    10) 闭包和函数遵循同样的lifetime省略规则

    这更多的是一个Rust的坑,而不是一个误解。
    虽然闭包是函数,但它并不遵循和函数同样的lifetime省略规则。(译注:这里的意思应该是闭包在使用上和函数相同,定义的语法也相似)

    1. fn function(x: &i32) -> &i32 {
    2. x
    3. }
    4. fn main() {
    5. let closure = |x: &i32| x;
    6. }

    抛错

    1. error: lifetime may not live long enough
    2. --> src/main.rs:6:29
    3. |
    4. 6 | let closure = |x: &i32| x;
    5. | - - ^ returning this value requires that `'1` must outlive `'2`
    6. | | |
    7. | | return type of closure is &'2 i32
    8. | let's call the lifetime of this reference `'1`

    补全后我们得到:

    1. // input lifetime gets applied to output
    2. fn function<'a>(x: &'a i32) -> &'a i32 {
    3. x
    4. }
    5. fn main() {
    6. // input and output each get their own distinct lifetimes
    7. let closure = for<'a, 'b> |x: &'a i32| -> &'b i32 { x };
    8. // note: the above line is not valid syntax, but we need it for illustrative purposes
    9. }

    这种不一致并没有什么原因。闭包最初是用和函数不同的类型推导语义实现的,现在我们也不能改变了,因为把它们统一起来会是一个不兼容改动。那么我们要如何显式地标注一个闭包的类型呢?我们的选择包括:

    1. fn main() {
    2. // cast to trait object, becomes unsized, oops, compile error
    3. let identity: dyn Fn(&i32) -> &i32 = |x: &i32| x;
    4. // can allocate it on the heap as a workaround but feels clunky
    5. let identity: Box<dyn Fn(&i32) -> &i32> = Box::new(|x: &i32| x);
    6. // can skip the allocation and just create a static reference
    7. let identity: &dyn Fn(&i32) -> &i32 = &|x: &i32| x;
    8. // previous line desugared :)
    9. let identity: &'static (dyn for<'a> Fn(&'a i32) -> &'a i32 + 'static) = &|x: &i32| -> &i32 { x };
    10. // this would be ideal but it's invalid syntax
    11. let identity: impl Fn(&i32) -> &i32 = |x: &i32| x;
    12. // this would also be nice but it's also invalid syntax
    13. let identity = for<'a> |x: &'a i32| -> &'a i32 { x };
    14. // since "impl trait" works in the function return position
    15. fn return_identity() -> impl Fn(&i32) -> &i32 {
    16. |x| x
    17. }
    18. let identity = return_identity();
    19. // more generic version of the previous solution
    20. fn annotate<T, F>(f: F) -> F where F: Fn(&T) -> &T {
    21. f
    22. }
    23. let identity = annotate(|x: &i32| x);
    24. }

    你肯定已经从上面的例子里发现了,当闭包类型被用作trait约束时,它们确实遵循正常的函数lifetime省略规则。
    这里并没有什么教训或者见解,它是怎样就是怎样。
    (译注:如果只是一个不捕获环境的闭包,可以这样写:let closure: for<’a> fn(&’a i32) -> &’a i32 = |x: &i32| x;。在nightly上其实已经可以这样写了:let identity: impl Fn(&i32) -> &i32 = |x: &i32| x;。实在不行我们还有无敌的unsafe:let closure = |x: &i32| unsafe { &(x as const _) };)
    要点

  • 每种语言都有坑

    11) ‘static引用总能被转换成’a引用

    我之前展示过如下代码:

    1. fn get_str<'a>() -> &'a str; // generic version
    2. fn get_str() -> &'static str; // 'static version

    有几个读者联系我,询问这两者在实践上是否有区别。最初我并不确定,调研了一番之后,不幸的结论是答案是yes,这两个函数在实践上确实有区别。
    一般情况下,当处理值时,我们可以在需要’a引用时使用’static引用,因为Rust会自动地把’static引用转换为’a引用。直觉上这是正确的,因为当需要一个短的lifetime时,使用一个长的lifetime不会导致任何内存安全问题。下面的程序符合预期地编译成功:

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

    然而当引用是一个函数的类型签名的一部分时,这个转换并不会发生,因此如下代码编译失败:

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

    抛出错误:

    1. error[E0597]: `some_string` does not live long enough
    2. --> src/main.rs:23:21
    3. |
    4. 23 | let some_str = &some_string[..];
    5. | ^^^^^^^^^^^ borrowed value does not live long enough
    6. ...
    7. 25 | let str_ref = a_or_b_fn(some_str, static_str_fn);
    8. | ---------------------------------- argument requires that `some_string` is borrowed for `'static`
    9. 26 | }
    10. | - `some_string` dropped here while still borrowed

    至于这是不是一个Rust的坑,是有争议的。这并不是一个简单直接的把&’static str转换为&’a str,而是把for Fn() -> &’static T转换为for<’a, T> Fn() -> &’a T。前者是值的转换,而后者是类型的转换。
    要点:

  • 签名为for<’a, T> fn() -> &’a T的函数比签名为for fn() -> &’static T的函数更灵活,适用于更多的场景

    结论

  • T是&T和&mut T的超集

  • &T和&mut T是不相交的集合
  • T: ‘static应该读作“T被’static lifetime约束”
  • 如果T: ‘static,那么T可以是一个有’static lifetime的借用类型,或者是一个独立类型
  • T: ‘static包括了所有独立类型,也就意味着T
    • 可以在运行时动态创建
    • 不需要在整个程序运行时有效
    • 可以被安全和随意地改变
    • 可以在运行时动态地drop
    • 可以拥有任意的lifetime
  • T: ‘a比&’a T更通用和灵活
  • T: ‘a接受独立类型,含有引用的独立类型,以及引用
  • &’a T只接受引用
  • 如果T: ‘static,那么T: ‘a,因为任意’a均满足’static >= ‘a
  • 几乎所有Rust代码都是泛型代码,到处都是省略的lifetime标注
  • Rust的lifetime省略规则并不是所有情形都总是正确的
  • Rust并不比你更懂你程序的语义
  • 给你的lifetime标注有意义的名字
  • 当添加显式lifetime标注时,想清楚加在哪和为什么
  • 所有的trait object都有推导出来的默认lifetime约束
  • Rust编译器的错误消息建议的修复方式,会让你的程序通过编译,但这和能让你的程序通过编译并且最符合你的程序的需求是两回事
  • lifetime是在编译时静态验证的
  • 在运行时,lifetime不可能以任何方式伸长缩短或改变
  • Rust的borrow checker总是会假设所有的分支都会走到,为变量选择其中最短的lifetime
  • 不要试图把可变引用重借用为共享引用,否则你会不开心的
  • 重借用一个可变引用不会终结它的lifetime,即使它被drop
  • 每种语言都有坑
  • 签名为for<’a, T> fn() -> &’a T的函数比签名为for fn() -> &’static T的函数更灵活,适用于更多的场景

lifetime省略规则参考这里:Lifetime elision - The Rust Reference (rust-lang.org)

另外, ‘static 是属于关键字,而不是通常的生命周期参数标注。生命周期参数标注只能通过编译时进行推导判断约束 Keywords - The Rust Reference (rust-lang.org)