Rust生命周期常见误区(中英对照)全面指南

 更新时间:2023年11月08日 14:12:55   作者:张强  
这篇文章主要WEIDJAI 介绍了Rust生命周期常见误区(中英对照)的全面指南,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

译者:ssbunny(兔子不咬人)

这篇文章写得很好,网上也有诸多译文。之所以再译一次,是因为我对这些译文的质量不太满意。它们大多过于拗口,译文无法突出原文所表达的重点,有些甚至存在错译。我谨慎地再译一次,只为分享给你。

Intro(导言)

I've held all of these misconceptions at some point and I see many beginners struggle with these misconceptions today. Some of my terminology might be non-standard, so here's a table of shorthand phrases I use and what I intend for them to mean.

PhraseShorthand for
T1) a set containing all possible types or
2) some type within that set
owned typesome non-reference type, e.g. i32, String, Vec, etc
1) borrowed type or
2) ref type
some reference type regardless of mutability, e.g. &i32, &mut i32, etc
1) mut ref or
2) exclusive ref
exclusive mutable reference, i.e. &mut T
1) immut ref or
2) shared ref
shared immutable reference, i.e. &T

接下来要讲的这些误区我都曾陷入过,如今也看到许多初学者在其中挣扎。可能我使用的术语不标准,所以我列了个短语速记表,以阐述我想表达的意思。

短语含义
T1) 一个集合,包含所有可能的类型 或
2) 该集合中的某个类型
拥有所有权的类型一些非引用类型, 像是 i32, String, Vec 等
1) 借用类型 或
2) 引用类型
一些引用类型,无论可变性如何,像是 &i32, &mut i32 等
1) 可变引用 或
2) 独占引用
独占可变引用,如 &mut T
1) 不可变引用 或
2) 共享引用
共享不可变引用,如 &T

The Misconceptions(误区)

In a nutshell: A variable's lifetime is how long the data it points to can be statically verified by the compiler to be valid at its current memory address. I'll now spend the next ~6500 words going into more detail about where people commonly get confused.

一言以蔽之: 变量的生命周期是指编译器可静态验证变量指向的数据,在其当前内存地址的有效时间。接下来,我将用大约 6500 字(英文原文)的篇幅详细介绍大家通常会混淆的地方。

1) T only contains owned types

T 仅包含拥有所有权的类型)

This misconception is more about generics than lifetimes but generics and lifetimes are tightly intertwined in Rust so it's not possible to talk about one without also talking about the other. Anyway:

这一误区更多源自对泛型的错误理解,而非生命周期。但在 Rust 中,泛型和生命周期是紧密相连的,谈论其中之一时不可能规避另一个不谈。这么说吧:

When I first started learning Rust I understood that i32&i32, and &mut i32 are different types. I also understood that some generic type variable T represents a set which contains all possible types. However, despite understanding both of these things separately, I wasn't able to understand them together. In my newbie Rust mind this is how I thought generics worked:

当我刚开始学习 Rust 时,我知道 i32&i32 和 &mut i32 是不同的类型。我还知道泛型变量 T 代表一个集合,其中包含所有可能的类型。然而,尽管我分别理解了这两件事,却无法将它们放在一起理解。在我这个 Rust 新手的脑海中,认为泛型是这样工作的:

Type Variable 类型变量T&T&mut T
Examples 例子i32&i32&mut i32

T contains all owned types. &T contains all immutably borrowed types. &mut T contains all mutably borrowed types. T&T, and &mut T are disjoint finite sets. Nice, simple, clean, easy, intuitive, and completely totally wrong. This is how generics actually work in Rust:

T 包含所有拥有所有权的类型。&T 包含所有不可变引用类型。&mut T 包含所有可变引用类型。T&T 和 &mut T 是互不相交的有限集合。漂亮、简单、干净、容易、直观,但完全是大错特错。事实上,在 Rust 中泛型是这样工作的:

Type VariableT&T&mut T
Examplesi32, &i32, &mut i32, &&i32, &mut &mut i32, ...&i32, &&i32, &&mut i32, ...&mut i32, &mut &mut i32, &mut &i32, ...

T&T, and &mut T are all infinite sets, since it's possible to borrow a type ad-infinitum. T is a superset of both &T and &mut T&T and &mut T are disjoint sets. Here's a couple examples which validate these concepts:

其实 T&T 和 &mut T 都是无限集,因为可以无限借用一个类型。T 是 &T 和 &mut T 的超集。下面是几个验证这些概念的例子:

trait Trait {}
impl<T> Trait for T {}
impl<T> Trait for &T {} // ❌
impl<T> Trait for &mut T {} // ❌

The above program doesn't compile as expected:

上述代码无法如期编译:

error[E0119]: conflicting implementations of trait `Trait` for type `&_`:
 --> src/lib.rs:5:1
  |
3 | impl<T> Trait for T {}
  | ------------------- first implementation here
4 |
5 | impl<T> Trait for &T {}
  | ^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&_`

error[E0119]: conflicting implementations of trait `Trait` for type `&mut _`:
 --> src/lib.rs:7:1
  |
3 | impl<T> Trait for T {}
  | ------------------- first implementation here
...
7 | impl<T> Trait for &mut T {}
  | ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&mut _`

The compiler doesn't allow us to define an implementation of Trait for &T and &mut T since it would conflict with the implementation of Trait for T which already includes all of &T and &mut T. The program below compiles as expected, since &T and &mut T are disjoint:

编译器不允许我们为 &T 和 &mut T 定义 Trait 的实现,因为这会与 T 对 Trait 的实现冲突,后者已经包含了 &T 和 &mut T。由于 &T 和 &mut T 不相交,因此下面的代码可以按预期编译:

trait Trait {}
impl<T> Trait for &T {} // ✅
impl<T> Trait for &mut T {} // ✅

Key Takeaways

  • T is a superset of both &T and &mut T
  • &T and &mut T are disjoint sets

主要收获

  • T 是 &T 和 &mut T 的超集
  • &T 和 &mut T 是互不相交的集合

2) if T: 'static then T must be valid for the entire program

(若 T: 'static 则 T 必须在整个程序运行期间有效)

Misconception Corollaries

  • T: 'static should be read as "T has a 'static lifetime"
  • &'static T and T: 'static are the same thing
  • if T: 'static then T must be immutable
  • if T: 'static then T can only be created at compile time

误区延伸

  • T: 'static 被视作 "T 拥有 'static 生命周期"
  • &'static T 与 T: 'static 相同
  • 若 T: 'static 则 T 是不可变的
  • 若 T: 'static 则 T 只能在编译期创建

Most Rust beginners get introduced to the 'static lifetime for the first time in a code example that looks something like this:

多数 Rust 初学者第一次接触 'static 生命周期时,都见到过类似这种示例代码:

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

They get told that "str literal" is hardcoded into the compiled binary and is loaded into read-only memory at run-time so it's immutable and valid for the entire program and that's what makes it 'static. These concepts are further reinforced by the rules surrounding defining static variables using the static keyword.

这些初学者们被告知:"str literal" 已被硬编码到编译后的二进制文件中,并会在运行期加载到只读内存区,因此它是不可变的,在整个程序运行期间都有效,这正是它之所以称作 "static" 的原因。而 Rust 使用 static 关键字定义 static 变量的语法规则,更是进一步强化了这种观念。

// Note: This example is purely for illustrative purposes.
// Never use `static mut`. It's a footgun. There are
// safe patterns for global mutable singletons in Rust but
// those are outside the scope of this article.
// 注意:本例纯粹用于演示说明,切勿使用 `static mut`。它是一把双刃剑。
// 在 Rust 中有 safe 模式的全局可变单例,但这不在本文讨论范围。
static BYTES: [u8; 3] = [1, 2, 3];
static mut MUT_BYTES: [u8; 3] = [1, 2, 3];
fn main() {
   MUT_BYTES[0] = 99; // ❌ - mutating static is unsafe 修改静态变量是 unsafe 操作
    unsafe {
        MUT_BYTES[0] = 99;
        assert_eq!(99, MUT_BYTES[0]);
    }
}

Regarding static variables

  • they can only be created at compile-time
  • they should be immutable, mutating them is unsafe
  • they're valid for the entire program

关于静态变量

  • 它们只能在编译时创建
  • 它们是不可变的,改变它们是不安全的
  • 它们在整个程序运行期间有效

The 'static lifetime was probably named after the default lifetime of static variables, right? So it makes sense that the 'static lifetime has to follow all the same rules, right?

'static 生命周期可能得名于 static 变量的默认生命周期,是这样吗?因此可以合理地认为,static 生命周期必须遵循所有相同的规则,是这样吗?

Well yes, but a type with a 'static lifetime is different from a type bounded by a 'static lifetime. The latter can be dynamically allocated at run-time, can be safely and freely mutated, can be dropped, and can live for arbitrary durations.

是这样的,但具有 'static 生命周期的类型和受 'static 生命周期约束的类型是不同的概念。后者可以在运行期动态分配,可以安全、自由地修改,可以 drop,可以存活任意时长。

It's important at this point to distinguish &'static T from T: 'static.

在这一点上,区分 &'static T 和 T: 'static 至关重要。

&'static T is an immutable reference to some T that can be safely held indefinitely long, including up until the end of the program. This is only possible if T itself is immutable and does not move _after the reference was created_. T does not need to be created at compile-time. It's possible to generate random dynamically allocated data at run-time and return 'static references to it at the cost of leaking memory, e.g.

&'static T 是对 T 的不可变引用,该引用可以安全地、无限期驻留在内存中,甚至到程序结束。然而只有当 T 本身是不可变的,并且在创建引用后不会移动时,才有可能做到这一点。T 不需要在编译期创建。完全可以在运行期生成随机的动态分配数据,并以内存泄漏为代价返回对它的 'static 引用,例如:

use rand;
// generate random 'static str refs at run-time
// 运行期随机生成 'static str 引用
fn rand_str_generator() -> &'static str {
    let rand_string = rand::random::<u64>().to_string();
    Box::leak(rand_string.into_boxed_str())
}

T: 'static is some T that can be safely held indefinitely long, including up until the end of the program. T: 'static includes all &'static T however it also includes all owned types, like StringVec, etc. The owner of some data is guaranteed that data will never get invalidated as long as the owner holds onto it, therefore the owner can safely hold onto the data indefinitely long, including up until the end of the program. T: 'static should be read as "T is bounded by a 'static lifetime" not _"T has a 'static lifetime"_. A program to help illustrate these concepts:

T: 'static 则是指 T 本身可以安全地、无限期驻留在内存中,甚至到程序结束。T: 'static 既包括所有 &'static T,也包括所有拥有所有权的类型,如 StringVec 等。只要数据的所有者持有这些数据,就能保证其永不失效,也就是说所有者可以安全地、无限期地持有这些数据,直到程序结束。T: 'static 应被视作 “T 受 'static 生命周期约束”,而不是 “T 拥有 'static 生命周期”。用程序来说明这一概念:

use rand;
fn drop_static<T: 'static>(t: T) {
    std::mem::drop(t);
}
fn main() {
    let mut strings: Vec<String> = Vec::new();
    for _ in 0..10 {
        if rand::random() {
            // all the strings are randomly generated
            // and dynamically allocated at run-time
            // 所有字符串都是随机生成的,并在运行期动态分配
            let string = rand::random::<u64>().to_string();
            strings.push(string);
        }
    }
    // strings are owned types so they're bounded by 'static
    // strings 是拥有所有权的类型,因此它们受 'static 约束
    for mut string in strings {
        // all the strings are mutable
        // 所有字符串都是可变的
        string.push_str("a mutation");
        // all the strings are droppable
        // 而且都可以被 drop
        drop_static(string); // ✅
    }
    // all the strings have been invalidated before the end of the program
    // 在程序结束前,strings 都已失效
    println!("I am the end of the program");
}

Key Takeaways

  • T: 'static should be read as "T is bounded by a 'static lifetime"
  • if T: 'static then T can be a borrowed type with a 'static lifetime or an owned type
  • since T: 'static includes owned types that means T

    • can be dynamically allocated at run-time
    • does not have to be valid for the entire program
    • can be safely and freely mutated
    • can be dynamically dropped at run-time
    • can have lifetimes of different durations

主要收获

  • T: 'static 应被理解为 “T 受 'static 生命周期约束”
  • 若 T: 'static 则 T 可以是拥有 'static 生命周期的借用类型 或 拥有所有权的类型
  • 既然 T: 'static 包括拥有所有权的类型,便意味着 T

    • 可以在运行期动态分配
    • 不必在整个程序运行期间有效
    • 可以安全、自由地修改
    • 可以在运行期动态 drop
    • 可以有不同的生命周期

3) &'a T and T: 'a are the same thing(&'a T 和 T: 'a 相同)

This misconception is a generalized version of the one above.

这一误区其实是上一个的泛化。

&'a T requires and implies T: 'a since a reference to T of lifetime 'a cannot be valid for 'a if T itself is not valid for 'a. For example, the Rust compiler will never allow the construction of the type &'static Ref<'a, T> because if Ref is only valid for 'a we can't make a 'static reference to it.

&'a T 要求并隐含了 T: 'a,因为如果 T 本身对生命周期 'a 无效,那么生命周期为 'a 的 T 的引用更不可能对 'a 有效。比方说,Rust 编译器从不允许构造 &'static Ref<'a, T> 类型,正是因为如果 Ref 只对 'a 有效,就不可能对它进行 'static 引用。

T: 'a includes all &'a T but the reverse is not true.

T:'a 包括所有 &'a T,反之则不成立。

// only takes ref types bounded by 'a
// 只接受满足生命周期 'a 的引用类型
fn t_ref<'a, T: 'a>(t: &'a T) {}
// takes any types bounded by 'a
// 接受满足生命周期 'a 的所有类型
fn t_bound<'a, T: 'a>(t: T) {}
// owned type which contains a reference
// 拥有所有权的类型,其内部包含引用
struct Ref<'a, T: 'a>(&'a T);
fn main() {
    let string = String::from("string");
    t_bound(&string); // ✅
    t_bound(Ref(&string)); // ✅
    t_bound(&Ref(&string)); // ✅
    t_ref(&string); // ✅
    t_ref(Ref(&string)); // ❌ - expected ref, found struct
    t_ref(&Ref(&string)); // ✅
    // string var is bounded by 'static which is bounded by 'a
    // 字符串变量受 'static 约束,而 'static 受 'a 约束
    t_bound(string); // ✅
}

Key Takeaways

  • T: 'a is more general and more flexible than &'a T
  • T: 'a accepts owned types, owned types which contain references, and references
  • &'a T only accepts references
  • if T: 'static then T: 'a since 'static >= 'a for all 'a

主要收获

  • 与 &'a T 相比,T: 'a 更通用、更灵活
  • T: 'a 接受拥有所有权的类型(其内部可含有引用)、引用类型
  • &'a T 只接受引用类型
  • 若 T: 'static 则 T: 'a ,因为对于所有 'a 都有 'static >= 'a

4) my code isn't generic and doesn't have lifetimes

(我的代码没使用泛型也不含生命周期注解)

Misconception Corollaries

  • it's possible to avoid using generics and lifetimes

误区延伸

  • 可以避免使用泛型和生命周期注解

This comforting misconception is kept alive thanks to Rust's lifetime elision rules, which allow you to omit lifetime annotations in functions because the Rust borrow checker will infer them following these rules:

  • every input ref to a function gets a distinct lifetime
  • if there's exactly one input lifetime it gets applied to all output refs
  • if there's multiple input lifetimes but one of them is &self or &mut self then the lifetime of self is applied to all output refs
  • otherwise output lifetimes have to be made explicit

这一看似令人舒适的误区祸起自 Rust 的生命周期省略规则(lifetime elision rules),它允许在函数中省略生命周期注解。之所以能够省略,是因为 Rust 的借用检查器可以基于以下规则推断出相应的注解:

  • 函数的每个输入引用都有一个独立的生命周期
  • 如果有且只有一个输入生命周期,该生命周期将应用于所有输出引用
  • 如果有多个输入生命周期,但其中一个是 &self 或 &mut self,那么 self 的生命周期将应用于所有输出引用
  • 否则,必须明确指出输出生命周期

That's a lot to take in so let's look at some examples:

要理解的有点多,不妨来看一些例子:

// elided 省略形式
fn print(s: &str);
// expanded 完整形式
fn print<'a>(s: &'a str);
// elided 省略形式
fn trim(s: &str) -> &str;
// expanded 完整形式
fn trim<'a>(s: &'a str) -> &'a str;
// illegal, can't determine output lifetime, no inputs
// 非法,无法确定输出生命周期,无输入
fn get_str() -> &str;
// explicit options include
// 显式标注
fn get_str<'a>() -> &'a str; // generic version 泛型版本
fn get_str() -> &'static str; // 'static version 'static 版本
// illegal, can't determine output lifetime, multiple inputs
// 非法,无法确定输出生命周期,多输入
fn overlap(s: &str, t: &str) -> &str;
// explicit (but still partially elided) options include
// 显式标注(但仍有部分标注被省略)
fn overlap<'a>(s: &'a str, t: &str) -> &'a str; // output can't outlive s 返回值的生命周期不长于 s
fn overlap<'a>(s: &str, t: &'a str) -> &'a str; // output can't outlive t 返回值的生命周期不长于 t
fn overlap<'a>(s: &'a str, t: &'a str) -> &'a str; // output can't outlive s & t 返回值的生命周期不长于 s & t
fn overlap(s: &str, t: &str) -> &'static str; // output can outlive s & t 返回值的生命周期可以长于 s & t
fn overlap<'a>(s: &str, t: &str) -> &'a str; // no relationship between input & output lifetimes 返回值的生命周期与输入无关
// expanded 完整形式
fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'a str;
fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'b str;
fn overlap<'a>(s: &'a str, t: &'a str) -> &'a str;
fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'static str;
fn overlap<'a, 'b, 'c>(s: &'a str, t: &'b str) -> &'c str;
// elided 省略形式
fn compare(&self, s: &str) -> &str;
// expanded 完整形式
fn compare<'a, 'b>(&'a self, &'b str) -> &'a str;

If you've ever written

  • a struct method
  • a function which takes references
  • a function which returns references
  • a generic function
  • a trait object (more on this later)
  • a closure (more on this later)

then your code has generic elided lifetime annotations all over it.

如果你曾写过

  • struct 方法
  • 获取引用的函数
  • 返回引用的函数
  • 泛型函数
  • trait 对象(稍后详述)
  • 闭包(稍后详述)

那么你的代码中就遍布省略的泛型生命周期注解。

Key Takeaways

  • almost all Rust code is generic code and there's elided lifetime annotations everywhere

主要收获

  • 几乎所有 Rust 代码都是泛型代码,四处皆是省略的生命周期注解

5) if it compiles then my lifetime annotations are correct

(只要编译成功,生命周期注解就是正确的)

Misconception Corollaries

  • Rust's lifetime elision rules for functions are always right
  • Rust's borrow checker is always right, technically and semantically
  • Rust knows more about the semantics of my program than I do

误区延伸

  • Rust 的函数生命周期省略规则总是正确的
  • Rust 的借用检查器在技术上和 语义上 总是正确的
  • Rust 比我更了解程序的语义

It's possible for a Rust program to be technically compilable but still semantically wrong. Take this for example:

Rust 程序有可能在技术上可以编译,但在语义上仍然是错误的。举个例子:

struct ByteIter<'a> {
    remainder: &'a [u8]
}

impl<'a> ByteIter<'a> {
    fn next(&mut self) -> Option<&u8> {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &self.remainder[0];
            self.remainder = &self.remainder[1..];
            Some(byte)
        }
    }
}

fn main() {
    let mut bytes = ByteIter { remainder: b"1" };
    assert_eq!(Some(&b'1'), bytes.next());
    assert_eq!(None, bytes.next());
}

ByteIter is an iterator that iterates over a slice of bytes. We're skipping the Iterator trait implementation for conciseness. It seems to work fine, but what if we want to check a couple bytes at a time?

ByteIter 是一个用来迭代字节切片的迭代器。为了简洁起见,我们跳过了 Iterator trait 的实现。目前一切正常,但如果我们想同时查看一对字节呢?

fn main() {
    let mut bytes = ByteIter { remainder: b"1123" };
    let byte_1 = bytes.next();
    let byte_2 = bytes.next();
    if byte_1 == byte_2 { // ❌
        // do something
    }
}

Uh oh! Compile error:

呦!编译错误:

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

I guess we can copy each byte. Copying is okay when we're working with bytes but if we turned ByteIter into a generic slice iterator that can iterate over any &'a [T] then we might want to use it in the future with types that may be very expensive or impossible to copy and clone. Oh well, I guess there's nothing we can do about that, the code compiles so the lifetime annotations must be right, right?

可以逐个复制字节来解决此编译错误。确实,在处理字节时复制是没问题的,但如果打算把 ByteIter 做成一个通用的切片迭代器,可以遍历任何 &'a [T],那就有可能把它用在复制或克隆成本很高的类型上,甚至是不可能复制或克隆的类型上。好吧,我想咱们对此都无能为力,代码能编译,那么生命周期注解一定是正确的,对吗?

Nope, the current lifetime annotations are actually the source of the bug! It's particularly hard to spot because the buggy lifetime annotations are elided. Let's expand the elided lifetimes to get a clearer look at the problem:

不对,当前的生命周期注解实际上正是 bug 的根源!该 bug 特别难以发现,因为错误的生命周期注释被省略掉了。我们来补充上被省略的生命周期,以便更清楚地了解问题所在:

struct ByteIter<'a> {
    remainder: &'a [u8]
}
impl<'a> ByteIter<'a> {
    fn next<'b>(&'b mut self) -> Option<&'b u8> {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &self.remainder[0];
            self.remainder = &self.remainder[1..];
            Some(byte)
        }
    }
}

That didn't help at all. I'm still confused. Here's a hot tip that only Rust pros know: give your lifetime annotations descriptive names. Let's try again:

一点帮助都没有,看起来还是一头雾水。此处有个只有 Rust 高手才知道的小窍门:给生命周期注解起个有意义的名字。来,再试一次:

struct ByteIter<'remainder> {
    remainder: &'remainder [u8]
}
impl<'remainder> ByteIter<'remainder> {
    fn next<'mut_self>(&'mut_self mut self) -> Option<&'mut_self u8> {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &self.remainder[0];
            self.remainder = &self.remainder[1..];
            Some(byte)
        }
    }
}

Each returned byte is annotated with 'mut_self but the bytes are clearly coming from 'remainder! Let's fix it.

每个返回的字节都被注解为 'mut_self,但这些字节显然来自 'remainder !来,搞定它。

struct ByteIter<'remainder> {
    remainder: &'remainder [u8]
}
impl<'remainder> ByteIter<'remainder> {
    fn next(&mut self) -> Option<&'remainder u8> {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &self.remainder[0];
            self.remainder = &self.remainder[1..];
            Some(byte)
        }
    }
}
fn main() {
    let mut bytes = ByteIter { remainder: b"1123" };
    let byte_1 = bytes.next();
    let byte_2 = bytes.next();
    // we can even drop the iterator now!
    // 调整后甚至可以 drop 掉迭代器
    std::mem::drop(bytes);
    if byte_1 == byte_2 { // ✅
        // do something
    }
}

Now that we look back on the previous version of our program it was obviously wrong, so why did Rust compile it? The answer is simple: it was memory safe.

现在回过头来看看上一个版本的代码,既然它是错误的,Rust 为什么要编译它呢?原因很简单:它是内存安全的。

The Rust borrow checker only cares about the lifetime annotations in a program to the extent it can use them to statically verify the memory safety of the program. Rust will happily compile programs even if the lifetime annotations have semantic errors, and the consequence of this is that the program becomes unnecessarily restrictive.

Rust 借用检查器只要能利用生命周期注解静态验证程序的内存安全性就够了,多余的事情不再关心。即使生命周期注解存在语义错误,Rust 也乐于编译它,哪怕会给程序带来不必要的限制。

Here's a quick example that's the opposite of the previous example: Rust's lifetime elision rules happen to be semantically correct in this instance but we unintentionally write a very restrictive method with our own unnecessary explicit lifetime annotations.

来看一个与上面相反的例子:示例中,Rust 的生命周期省略规则语义上正确,但我们却无意中写出了一个限制极严的方法,并使用了不必要的显式生命周期注解。

#[derive(Debug)]
struct NumRef<'a>(&'a i32);
impl<'a> NumRef<'a> {
    // my struct is generic over 'a so that means I need to annotate
    // my self parameters with 'a too, right? (answer: no, not right)
    // 结构体的泛型是 'a ,是否意味着
    // 也需要用 'a 来注解 self 参数?(答案:否)
    fn some_method(&'a mut self) {}
}
fn main() {
    let mut num_ref = NumRef(&5);
    // mutably borrows num_ref for the rest of its lifetime
    // 在其生命周期内可变地借用 num_ref
    num_ref.some_method();
    num_ref.some_method(); // ❌
    println!("{:?}", num_ref); // ❌
}

If we have some struct generic over 'a we almost never want to write a method with a &'a mut self receiver. What we're communicating to Rust is _"this method will mutably borrow the struct for the entirety of the struct's lifetime"_. In practice this means Rust's borrow checker will only allow at most one call to some_method before the struct becomes permanently mutably borrowed and thus unusable. The use-cases for this are extremely rare but the code above is very easy for confused beginners to write and it compiles. The fix is to not add unnecessary explicit lifetime annotations and let Rust's lifetime elision rules handle it:

当 struct 存在泛型参数 'a 时,几乎永远不会再写一个接收 &'a mut self 的方法,因为这样写相当于告诉 Rust _"该方法将在 struct 的整个生命周期内可变地借用该 struct"_。实践中意味着 Rust 的借用检查器最多只允许调用 some_method 一次,之后 struct 就永久地被可变借用走,从而无法再使用。这种使用场景极其罕见,但对于懵懂的初学者来说,却非常容易编写出上面这类代码,关键它还能编译通过。解决方法是不去添加不必要的显式生命周期注解,交由 Rust 的生命周期省略规则处理:

#[derive(Debug)]
struct NumRef<'a>(&'a i32);
impl<'a> NumRef<'a> {
    // no more 'a on mut self
    // mut self 上不再使用 'a
    fn some_method(&mut self) {}
    // above line desugars to
    // 去掉语法糖后相当于
    fn some_method_desugared<'b>(&'b mut self){}
}
fn main() {
    let mut num_ref = NumRef(&5);
    num_ref.some_method();
    num_ref.some_method(); // ✅
    println!("{:?}", num_ref); // ✅
}

Key Takeaways

  • Rust's lifetime elision rules for functions are not always right for every situation
  • Rust does not know more about the semantics of your program than you do
  • give your lifetime annotations descriptive names
  • try to be mindful of where you place explicit lifetime annotations and why

主要收获

  • Rust 的函数生命周期省略规则并不总是适用于所有情况
  • Rust 并不比你更了解程序的语义
  • 为生命周期注解赋予有意义的名称
  • 谨慎考虑在何处放置显式生命周期注解以及为什么要这样做

6) boxed trait objects don't have lifetimes

(装箱后的 trait 对象没有生命周期)

Earlier we discussed Rust's lifetime elision rules _for functions_. Rust also has lifetime elision rules for trait objects, which are:

  • if a trait object is used as a type argument to a generic type then its life bound is inferred from the containing type

    • if there's a unique bound from the containing then that's used
    • if there's more than one bound from the containing type then an explicit bound must be specified
  • if the above doesn't apply then

    • if the trait is defined with a single lifetime bound then that bound is used
    • if 'static is used for any lifetime bound then 'static is used
    • if the trait has no lifetime bounds then its lifetime is inferred in expressions and is 'static outside of expressions

前面我们讨论了 Rust 针对函数的生命周期省略规则。Rust 也有针对 trait 对象的生命周期省略规则,它们是:

  • 如果 trait 对象被用作泛型的类型参数,那么它的生命周期约束从包含类型中推断

    • 如果在包含类型中存在唯一一个约束,则使用该约束
    • 如果在包含类型中存在多个约束,则必须指定显式约束
  • 如果上述情况不适用,则

    • 如果 trait 的定义只有一个生命周期约束,则使用该约束
    • 如果 'static 被用于任何生命周期约束,则使用 'static
    • 如果 trait 没有生命周期约束,则在表达式中推断生命周期,并在表达式外使用 'static

All of that sounds super complicated but can be simply summarized as "a trait object's lifetime bound is inferred from context." After looking at a handful of examples we'll see the lifetime bound inferences are pretty intuitive so we don't have to memorize the formal rules:

这些听起来超复杂,但可以简单概括为 "根据上下文推断出 trait 对象的生命周期约束" 。看几个例子后你就会发现,生命周期约束推断非常直观,根本不必记住正式的规则:

use std::cell::Ref;
trait Trait {}
// elided 省略形式
type T1 = Box<dyn Trait>;
// expanded, Box<T> has no lifetime bound on T, so inferred as 'static
// 完整形式,Box<T> 对 T 没有生命周期约束,因此推断为 'static
type T2 = Box<dyn Trait + 'static>;
// elided 省略形式
impl dyn Trait {}
// expanded 完整形式
impl dyn Trait + 'static {}
// elided 省略形式
type T3<'a> = &'a dyn Trait;
// expanded, &'a T requires T: 'a, so inferred as 'a
// 完整形式,&'a T 要求 T: 'a,因此推断为 'a
type T4<'a> = &'a (dyn Trait + 'a);
// elided 省略形式
type T5<'a> = Ref<'a, dyn Trait>;
// expanded, Ref<'a, T> requires T: 'a, so inferred as 'a
type T6<'a> = Ref<'a, dyn Trait + 'a>;
trait GenericTrait<'a>: 'a {}
// elided 省略形式
type T7<'a> = Box<dyn GenericTrait<'a>>;
// expanded 完整形式
type T8<'a> = Box<dyn GenericTrait<'a> + 'a>;
// elided 省略形式
impl<'a> dyn GenericTrait<'a> {}
// expanded 完整形式
impl<'a> dyn GenericTrait<'a> + 'a {}

Concrete types which implement traits can have references and thus they also have lifetime bounds, and so their corresponding trait objects have lifetime bounds. Also you can implement traits directly for references which obviously have lifetime bounds:

实现了 trait 的具体类型可以包含引用,因此它们也有生命周期约束,继而它们对应的 trait 对象也有生命周期约束。此外,还可以直接为引用实现 trait,而引用显然也有生命周期约束:

trait Trait {}
struct Struct {}
struct Ref<'a, T>(&'a T);
impl Trait for Struct {}
// impl Trait directly on a ref type
// 在引用类型上直接实现 Trait
impl Trait for &Struct {}
// impl Trait on a type containing refs
// 在包含引用的类型上实现 Trait
impl<'a, T> Trait for Ref<'a, T> {}

Anyway, this is worth going over because it often confuses beginners when they refactor a function from using trait objects to generics or vice versa. Take this program for example:

总之,这点很值得强调,因为初学者将函数从使用 trait 对象重构为使用泛型(或反之)时,经常会感到困惑。以此程序为例:

use std::fmt::Display;
fn dynamic_thread_print(t: Box<dyn Display + Send>) {
    std::thread::spawn(move || {
        println!("{}", t);
    }).join();
}
fn static_thread_print<T: Display + Send>(t: T) { // ❌
    std::thread::spawn(move || {
        println!("{}", t);
    }).join();
}

It throws this compile error:

程序抛出编译错误:

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

Okay great, the compiler tells us how to fix the issue so let's fix the issue.

好极了,编译器告知了如何解决问题,那就按它说的来解决一下吧。

use std::fmt::Display;

fn dynamic_thread_print(t: Box<dyn Display + Send>) {
    std::thread::spawn(move || {
        println!("{}", t);
    }).join();
}

fn static_thread_print<T: Display + Send + 'static>(t: T) { // ✅
    std::thread::spawn(move || {
        println!("{}", t);
    }).join();
}

It compiles now but these two functions look awkward next to each other, why does the second function require a 'static bound on T where the first function doesn't? That's a trick question. Using the lifetime elision rules Rust automatically infers a 'static bound in the first function so both actually have 'static bounds. This is what the Rust compiler sees:

现在可以编译了,但这两个函数放在一起看时会很别扭:为什么第二个函数需要对 T 进行 'static 约束,而第一个函数不需要呢?令人迷惑。其实 Rust 使用生命周期省略规则自动在第一个函数中推断出了 'static 约束,因此这两个函数实际上都有 'static 约束。下面才是 Rust 编译器看到的:

use std::fmt::Display;

fn dynamic_thread_print(t: Box<dyn Display + Send + 'static>) {
    std::thread::spawn(move || {
        println!("{}", t);
    }).join();
}

fn static_thread_print<T: Display + Send + 'static>(t: T) {
    std::thread::spawn(move || {
        println!("{}", t);
    }).join();
}

Key Takeaways

  • all trait objects have some inferred default lifetime bounds

主要收获

  • 所有 trait 对象都有隐含的默认生命周期约束

7) compiler error messages will tell me how to fix my program

(编译器的错误信息足以指导修复程序)

Misconception Corollaries

  • Rust's lifetime elision rules for trait objects are always right
  • Rust knows more about the semantics of my program than I do

误区延伸

  • Rust 针对 trait 对象的生命周期省略规则总是正确的
  • Rust 比我更了解程序的语义

This misconception is the previous two misconceptions combined into one example:

这一误区刚好是将前两个合二为一的范例:

use std::fmt::Display;
fn box_displayable<T: Display>(t: T) -> Box<dyn Display> { // ❌
    Box::new(t)
}

Throws this error:

抛出错误:

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

Okay, let's fix it how the compiler is telling us to fix it, nevermind the fact that it's automatically inferring a 'static lifetime bound for our boxed trait object without telling us and its recommended fix is based on that unstated fact:

好,我们来按照编译器说的方式修复该问题,别忘了它自动为装箱后的 trait 对象推断出了 'static 生命周期约束,而编译器推荐的解决方式正是基于这一未说明的事实:

use std::fmt::Display;

fn box_displayable<T: Display + 'static>(t: T) -> Box<dyn Display> { // ✅
    Box::new(t)
}

So the program compiles now... but is this what we actually want? Probably, but maybe not. The compiler didn't mention any other fixes but this would have also been appropriate:

程序现在可以编译了...但这就是我们想要的吗?也许是,也许不是。编译器没有提到其他修复方式,但这样其实也可以:

use std::fmt::Display;

fn box_displayable<'a, T: Display + 'a>(t: T) -> Box<dyn Display + 'a> { // ✅
    Box::new(t)
}

This function accepts all the same arguments as the previous version plus a lot more! Does that make it better? Not necessarily, it depends on the requirements and constraints of our program. This example is a bit abstract so let's take a look at a simpler and more obvious case:

该函数不仅兼容前一版本的所有参数,还能接受更多参数!然而这样就更好吗?也不一定,这取决于程序的要求和限制。该例略为抽象,再来看个更简单、更明显的例子:

fn return_first(a: &str, b: &str) -> &str { // ❌
    a
}

Throws:

抛出:

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

The error message recommends annotating both inputs and the output with the same lifetime. If we did this our program would compile but this function would overly-constrain the return type. What we actually want is this:

错误信息建议将输入、输出标注为相同的生命周期。如果按它说的做,程序确实可以编译,但该函数会过度约束返回类型。实际上,我们想要的是:

fn return_first<'a>(a: &'a str, b: &str) -> &'a str { // ✅
    a
}

Key Takeaways

  • Rust's lifetime elision rules for trait objects are not always right for every situation
  • Rust does not know more about the semantics of your program than you do
  • Rust compiler error messages suggest fixes which will make your program compile which is not that same as fixes which will make you program compile and best suit the requirements of your program

主要收获

  • Rust 针对 trait 对象的生命周期省略规则并非适合每种情况
  • Rust 不会比你更了解程序的语义
  • Rust 编译器错误信息所建议的修复方法可以使程序编译成功,但这并不等同于可以使程序编译成功 并且 最符合要求。

8) lifetimes can grow and shrink at run-time

(生命周期可以在运行期增长或缩短)

Misconception Corollaries

  • container types can swap references at run-time to change their lifetime
  • Rust borrow checker does advanced control flow analysis

误区延伸

  • 容器类型可在运行期交换引用以改变其生命周期
  • Rust 借用检查器可进行高级控制流分析

This does not compile:

以下代码无法编译:

struct Has<'lifetime> {
    lifetime: &'lifetime str,
}
fn main() {
    let long = String::from("long");
    let mut has = Has { lifetime: &long };
    assert_eq!(has.lifetime, "long");
    {
        let short = String::from("short");
        // "switch" to short lifetime
        // “切换”到 short 生命周期
        has.lifetime = &short;
        assert_eq!(has.lifetime, "short");
        // "switch back" to long lifetime (but not really)
        // “切换回” long 生命周期(其实并没有)
        has.lifetime = &long;
        assert_eq!(has.lifetime, "long");
        // `short` dropped here
        // `short` 在此处被 drop
    }
    // ❌ - `short` still "borrowed" after drop
    // ❌ - `short` 在 drop 后仍处于 “借用” 状态
    assert_eq!(has.lifetime, "long"); 
}

It throws:

它会抛出:

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

This also does not compile, throws the exact same error as above:

改成下面这样也无法编译,它会抛出与上面完全相同的错误:

struct Has<'lifetime> {
    lifetime: &'lifetime str,
}

fn main() {
    let long = String::from("long");
    let mut has = Has { lifetime: &long };
    assert_eq!(has.lifetime, "long");

    // this block will never run
    // 该代码块不会执行
    if false {
        let short = String::from("short");
        // "switch" to short lifetime
        // “切换”到 short 生命周期
        has.lifetime = &short;
        assert_eq!(has.lifetime, "short");

        // "switch back" to long lifetime (but not really)
        // “切换回” long 生命周期(其实并没有)
        has.lifetime = &long;
        assert_eq!(has.lifetime, "long");
        // `short` dropped here
        // `short` 在此处被 drop
    }
    // ❌ - `short` still "borrowed" after drop
    // ❌ - `short` 在 drop 后仍处于 “借用” 状态
    assert_eq!(has.lifetime, "long"); 
}

Lifetimes have to be statically verified at compile-time and the Rust borrow checker only does very basic control flow analysis, so it assumes every block in an if-else statement and every match arm in a match statement can be taken and then chooses the shortest possible lifetime for the variable. Once a variable is bounded by a lifetime it is bounded by that lifetime _forever_. The lifetime of a variable can only shrink, and all the shrinkage is determined at compile-time.

生命周期必须在编译期进行静态验证,而 Rust 借用检查器也只能进行非常基础的控制流分析,因此它假定 if-else 语句和 match 语句中的每个分支代码块都将被执行[译注:Rust 编译器采用了流敏感分析(flow-sensitive analyses)],然后为变量选择最短的生命周期。变量的生命周期一旦被确定,就会永远受该生命周期约束。变量的生命周期只能缩短,而所有的缩短都会在编译期决定。

Key Takeaways

  • lifetimes are statically verified at compile-time
  • lifetimes cannot grow or shrink or change in any way at run-time
  • Rust borrow checker will always choose the shortest possible lifetime for a variable assuming all code paths can be taken

主要收获

  • 生命周期在编译期进行静态验证
  • 生命周期不能在运行期以任何方式增长、缩短或改变
  • Rust 借用检查器总是假定所有代码路径都会被执行,然后为变量选择最短的生命周期

9) downgrading mut refs to shared refs is safe

(将可变引用降级为共享引用是安全操作)

Misconception Corollaries

  • re-borrowing a reference ends its lifetime and starts a new one

误区延伸

  • 重新借用引用会结束其原有生命周期,并开始新的生命周期

You can pass a mut ref to a function expecting a shared ref because Rust will implicitly re-borrow the mut ref as immutable:

可以将可变引用传递给期望使用共享引用的函数,Rust 会隐式地重新借用可变引用,并将其视为不可变:

fn takes_shared_ref(n: &i32) {}

fn main() {
    let mut a = 10;
    takes_shared_ref(&mut a); // ✅
    takes_shared_ref(&*(&mut a)); // above line desugared 上行代码去掉语法糖后
}

Intuitively this makes sense, since there's no harm in re-borrowing a mut ref as immutable, right? Surprisingly no, as the program below does not compile:

直觉上这也没问题,毕竟重新借用一个可变引用并将其视为不可变的,没什么毛病,对吧?令人惊讶的是,情况并非如此,下面的程序无法编译:

fn main() {
    let mut a = 10;
    let b: &i32 = &*(&mut a); // re-borrowed as immutable 重新借用为不可变引用
    let c: &i32 = &a;
    dbg!(b, c); // ❌
}

Throws this error:

抛出错误:

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

A mutable borrow does occur, but it's immediately and unconditionally re-borrowed as immutable and then dropped. Why is Rust treating the immutable re-borrow as if it still has the mut ref's exclusive lifetime? While there's no issue in the particular example above, allowing the ability to downgrade mut refs to shared refs does indeed introduce potential memory safety issues:

可变借用确实发生了,但它会被立即无条件地重新借用为不可变的,继而被 drop 掉。为什么 Rust 会把“不可变的重新借用”视作“仍具有可变引用”的独占生命周期呢?上例虽没有问题,但允许将可变引用降级为共享引用确实会带来潜在的内存安全问题:

use std::sync::Mutex;
struct Struct {
    mutex: Mutex<String>
}
impl Struct {
    // downgrades mut self to shared str
    // 将可变引用 self 降级为共享引用 str
    fn get_string(&mut self) -> &str {
        self.mutex.get_mut().unwrap()
    }
    fn mutate_string(&self) {
        // if Rust allowed downgrading mut refs to shared refs
        // then the following line would invalidate any shared
        // refs returned from the get_string method
        // 如果 Rust 允许将可变引用降级为共享引用,那么下面一行
        // 将使 get_string 方法返回的任何共享引用都失效
        *self.mutex.lock().unwrap() = "surprise!".to_owned();
    }
}
fn main() {
    let mut s = Struct {
        mutex: Mutex::new("string".to_owned())
    };
    // mut ref downgraded to shared ref 
    // 可变引用降级为共享引用
    let str_ref = s.get_string(); 
    // str_ref invalidated, now a dangling pointer
    // str_ref 已失效,成了个悬垂指针
    s.mutate_string(); 
    dbg!(str_ref); // ❌ - as expected! 一如所料!
}

The point here is that when you re-borrow a mut ref as a shared ref you don't get that shared ref without a big gotcha: it extends the mut ref's lifetime for the duration of the re-borrow even if the mut ref itself is dropped. Using the re-borrowed shared ref is very difficult because it's immutable but it can't overlap with any other shared refs. The re-borrowed shared ref has all the cons of a mut ref and all the cons of a shared ref and has the pros of neither. I believe re-borrowing a mut ref as a shared ref should be considered a Rust anti-pattern. Being aware of this anti-pattern is important so that you can easily spot it when you see code like this:

这里的重点是,重新将可变引用借用为共享引用,并不能顺利地获得共享引用:即使可变引用本身已被 drop,它也会在重新借用期间延长可变引用的生命周期。使用重新借用的共享引用非常困难,尽管它是不可变的,但它不能与任何其他共享引用重叠(译注:即,不能与其他引用同时访问相同的资源)。重新借用的共享引用既有可变引用的所有缺点,也有共享引用的所有缺点,而且还不具备两者的优点。所以我认为,“重新将可变引用借用为共享引用”应被视为 Rust 的反模式。意识到这种反模式很重要,这样当你看到类似代码时就能很快辨别它:

// downgrades mut T to shared T
// 将可变引用 T 降级为共享引用 T
fn some_function<T>(some_arg: &mut T) -> &T;
struct Struct;
impl Struct {
    // downgrades mut self to shared self
    // 将可变引用 self 降级为共享引用 self
    fn some_method(&mut self) -> &Self;
    // downgrades mut self to shared T
    // 将可变引用 self 降级为共享引用 T
    fn other_method(&mut self) -> &T;
}

Even if you avoid re-borrows in function and method signatures Rust still does automatic implicit re-borrows so it's easy to bump into this problem without realizing it like so:

即便在函数或方法签名中避免了重新借用,Rust 仍会自动进行隐式的重新借用,因此很容易在不知情的情况下遇到问题,例如:

use std::collections::HashMap;
type PlayerID = i32;
#[derive(Debug, Default)]
struct Player {
    score: i32,
}
fn start_game(player_a: PlayerID, player_b: PlayerID, server: &mut HashMap<PlayerID, Player>) {
    // get players from server or create & insert new players if they don't yet exist
    // 从 server 获取 player,若无,则创建并插入新 player
    let player_a: &Player = server.entry(player_a).or_default();
    let player_b: &Player = server.entry(player_b).or_default();
    // do something with players
    // 对 player 进行某些操作
    dbg!(player_a, player_b); // ❌
}

The above fails to compile. or_default() returns a &mut Player which we're implicitly re-borrowing as &Player because of our explicit type annotations. To do what we want we have to:

上述代码编译失败。由于我们显式地标注了数据类型,Rust 会隐式地将 or_default() 返回的 &mut Player 重新借用为 &Player。为了达成目的,我们必须:

use std::collections::HashMap;
type PlayerID = i32;
#[derive(Debug, Default)]
struct Player {
    score: i32,
}
fn start_game(player_a: PlayerID, player_b: PlayerID, server: &mut HashMap<PlayerID, Player>) {
    // drop the returned mut Player refs since we can't use them together anyway
    // 丢弃所有返回的 Player 可变引用,反正也不能同时使用它们
    server.entry(player_a).or_default();
    server.entry(player_b).or_default();
    // fetch the players again, getting them immutably this time, without any implicit re-borrows
    // 这次以不可变方式再次获取 player,而不会有任何隐式的重新借用。
    let player_a = server.get(&player_a);
    let player_b = server.get(&player_b);
    // do something with players
    // 对 player 进行某些操作
    dbg!(player_a, player_b); // ✅
}

Kinda awkward and clunky but this is the sacrifice we make at the Altar of Memory Safety.

虽然有些笨拙,而且不够优雅,但这是我们给“内存安全祭坛”献上的祭品。

Key Takeaways

  • try not to re-borrow mut refs as shared refs, or you're gonna have a bad time
  • re-borrowing a mut ref doesn't end its lifetime, even if the ref is dropped

主要收获

  • 尽量避免将可变引用重新借用为共享引用,否则会让你头大
  • 重新借用可变引用不会结束其生命周期,即使该引用已被 drop 掉

10) closures follow the same lifetime elision rules as functions

(闭包遵循与函数相同的生命周期省略规则)

This is more of a Rust Gotcha than a misconception.

这条更像是 Rust 中的一个陷阱,而不是误区。

Closures, despite being functions, do not follow the same lifetime elision rules as functions.

闭包,尽管也是函数,但并不遵循与函数相同的生命周期省略规则。

fn function(x: &i32) -> &i32 {
    x
}
fn main() {
    let closure = |x: &i32| x; // ❌
}

Throws:

抛出:

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

After desugaring we get:

(将省略规则)展开后得到:

// input lifetime gets applied to output
// 输入生命周期同时应用于输出
fn function<'a>(x: &'a i32) -> &'a i32 {
    x
}
fn main() {
    // input and output each get their own distinct lifetimes
    // 输入和输出各自具有独立的生命周期
    let closure = for<'a, 'b> |x: &'a i32| -> &'b i32 { x };
    // note: the above line is not valid syntax, but we need it for illustrative purposes
    // 注意:上行不是有效的语法,只是用它来说明问题
}

There's no good reason for this discrepancy. Closures were first implemented with different type inference semantics than functions and now we're stuck with it forever because to unify them at this point would be a breaking change. So how can we explicitly annotate a closure's type? Our options include:

造成这种差异的原因挺荒谬的。闭包在最一开始时便使用了与函数不同的类型推断语义,而现今我们永远都只能如此了,因为此时再统一它们将是个不兼容的改变。那么该如何显式标注闭包的类型呢?可选项有:

fn main() {
    // cast to trait object, becomes unsized, oops, compile error
    // 转换为 trait 对象,变成 unsized,呃,可能导致编译错误
    let identity: dyn Fn(&i32) -> &i32 = |x: &i32| x;
    // can allocate it on the heap as a workaround but feels clunky
    // 可以将其分配到堆上,倒也算个笨方法
    let identity: Box<dyn Fn(&i32) -> &i32> = Box::new(|x: &i32| x);
    // can skip the allocation and just create a static reference
    // 可以跳过分配过程,直接创建一个静态引用
    let identity: &dyn Fn(&i32) -> &i32 = &|x: &i32| x;
    // previous line desugared :)
    // 将上一行展开 :)
    let identity: &'static (dyn for<'a> Fn(&'a i32) -> &'a i32 + 'static) = &|x: &i32| -> &i32 { x };
    // this would be ideal but it's invalid syntax
    // 这样做似乎更理想,但它是无效语法
    let identity: impl Fn(&i32) -> &i32 = |x: &i32| x;
    // this would also be nice but it's also invalid syntax
    // 这样做也很好,但同样是无效语法
    let identity = for<'a> |x: &'a i32| -> &'a i32 { x };
    // since "impl trait" works in the function return position
    // 鉴于 "impl trait" 在函数返回值处有效
    fn return_identity() -> impl Fn(&i32) -> &i32 {
        |x| x
    }
    let identity = return_identity();
    // more generic version of the previous solution
    // 前一解决方案更通用的版本
    fn annotate<T, F>(f: F) -> F where F: Fn(&T) -> &T {
        f
    }
    let identity = annotate(|x: &i32| x);
}

As I'm sure you've already noticed from the examples above, when closure types are used as trait bounds they do follow the usual function lifetime elision rules.

从上面的示例中可以看出,当闭包类型用作 trait 约束时,它们确实遵循常规的函数生命周期省略规则。

There's no real lesson or insight to be had here, it just is what it is.

关于这一条没有什么经验教训或启示了,事情就是这样。

Key Takeaways

  • every language has gotchas 🤷

主要收获

  • 每种语言皆有陷阱 🤷

Conclusion(结论)

  • T is a superset of both &T and &mut T
  • &T and &mut T are disjoint sets
  • T: 'static should be read as "T is bounded by a 'static lifetime"
  • if T: 'static then T can be a borrowed type with a 'static lifetime or an owned type
  • since T: 'static includes owned types that means T

    • can be dynamically allocated at run-time
    • does not have to be valid for the entire program
    • can be safely and freely mutated
    • can be dynamically dropped at run-time
    • can have lifetimes of different durations
  • T: 'a is more general and more flexible than &'a T
  • T: 'a accepts owned types, owned types which contain references, and references
  • &'a T only accepts references
  • if T: 'static then T: 'a since 'static >= 'a for all 'a
  • almost all Rust code is generic code and there's elided lifetime annotations everywhere
  • Rust's lifetime elision rules are not always right for every situation
  • Rust does not know more about the semantics of your program than you do
  • give your lifetime annotations descriptive names
  • try to be mindful of where you place explicit lifetime annotations and why
  • all trait objects have some inferred default lifetime bounds
  • Rust compiler error messages suggest fixes which will make your program compile which is not that same as fixes which will make you program compile and best suit the requirements of your program
  • lifetimes are statically verified at compile-time
  • lifetimes cannot grow or shrink or change in any way at run-time
  • Rust borrow checker will always choose the shortest possible lifetime for a variable assuming all code paths can be taken
  • try not to re-borrow mut refs as shared refs, or you're gonna have a bad time
  • re-borrowing a mut ref doesn't end its lifetime, even if the ref is dropped
  • every language has gotchas 🤷
  • T 是 &T 和 &mut T 的超集
  • &T 和 &mut T 是互不相交的集合
  • T: 'static 应被理解为 “T 受 'static 生命周期约束”
  • 若 T: 'static 则 T 可以是拥有 'static 生命周期的借用类型 或 拥有所有权的类型
  • 既然 T: 'static 包括拥有所有权的类型,便意味着 T

    • 可以在运行期动态分配
    • 不必在整个程序运行期间有效
    • 可以安全、自由地修改
    • 可以在运行期动态 drop
    • 可以有不同的生命周期
  • 与 &'a T 相比,T: 'a 更通用、更灵活
  • T: 'a 接受拥有所有权的类型(其内部可含有引用)、引用类型
  • &'a T 只接受引用类型
  • 若 T: 'static 则 T: 'a ,因为对于所有 'a 都有 'static >= 'a
  • 几乎所有 Rust 代码都是泛型代码,四处皆是省略的生命周期注解
  • Rust 的函数生命周期省略规则并不总是适用于所有情况
  • Rust 并不比你更了解程序的语义
  • 为生命周期注解赋予有意义的名称
  • 谨慎考虑在何处放置显式生命周期注解以及为什么要这样做
  • 所有 trait 对象都有隐含的默认生命周期约束
  • Rust 针对 trait 对象的生命周期省略规则并非适合每种情况
  • Rust 不会比你更了解程序的语义
  • Rust 编译器错误信息所建议的修复方法可以使程序编译成功,但这并不等同于可以使程序编译成功 并且 最符合要求。
  • 生命周期在编译期进行静态验证
  • 生命周期不能在运行期以任何方式增长、缩短或改变
  • Rust 借用检查器总是假定所有代码路径都会被执行,然后为变量选择最短的生命周期
  • 尽量避免将可变引用重新借用为共享引用,否则会让你头大
  • 重新借用可变引用不会结束其生命周期,即使该引用已被 drop 掉
  • 每种语言皆有陷阱 🤷

以上就是Rust生命周期常见误区(中英对照)全面指南的详细内容,更多关于Rust生命周期的资料请关注脚本之家其它相关文章!

相关文章

  • Rust利用tauri制作个效率小工具

    Rust利用tauri制作个效率小工具

    日常使用电脑中经常会用到一个quicke工具中的轮盘菜单工具。但quicke免费版很多功能不支持,且它的触发逻辑用的不舒服,经常误触。所以本文就来用tauri自制一个小工具,希望对大家有所帮助
    2023-02-02
  • 探索 Rust 中实用的错误处理技巧

    探索 Rust 中实用的错误处理技巧

    探索Rust中实用的错误处理技巧!Rust是一门静态类型系统安全且高效的编程语言,但使用过程中难免会遇到各种错误,学会如何正确处理这些错误至关重要,本指南将为您提供一些实用的错误处理技巧,帮助您更好地编写健壮的代码,需要的朋友可以参考下
    2024-01-01
  • Rust结构体的定义与实例化详细讲解

    Rust结构体的定义与实例化详细讲解

    结构体是一种自定义的数据类型,它允许我们将多个不同的类型组合成一个整体。下面我们就来学习如何定义和使用结构体,并对比元组与结构体之间的异同,需要的可以参考一下
    2022-12-12
  • 如何用Rust打印hello world

    如何用Rust打印hello world

    这篇文章主要介绍了如何用Rust打印hello world,本文分步骤通过图文并茂的形式给大家讲解的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-09-09
  • 关于Rust 使用 dotenv 来设置环境变量的问题

    关于Rust 使用 dotenv 来设置环境变量的问题

    在项目中,我们通常需要设置一些环境变量,用来保存一些凭证或其它数据,这时我们可以使用dotenv这个crate,接下来通过本文给大家介绍Rust 使用dotenv来设置环境变量的问题,感兴趣的朋友一起看看吧
    2022-01-01
  • Rust 语言中的 into() 方法及代码实例

    Rust 语言中的 into() 方法及代码实例

    在 Rust 中,into() 方法通常用于将一个类型的值转换为另一个类型,这通常涉及到资源的所有权转移,本文给大家介绍Rust 语言中的 into() 方法及代码实例,感谢的朋友跟随小编一起看看吧
    2024-03-03
  • 如何使用Rust直接编译单个的Solidity合约

    如何使用Rust直接编译单个的Solidity合约

    本文介绍了如何使用Rust语言直接编译Solidity智能合约,特别适用于没有外部依赖或flatten后的合约,一般情况下,Solidity开发者使用Hardhat或Foundry框架,本文给大家介绍如何使用Rust直接编译单个的Solidity合约,感兴趣的朋友一起看看吧
    2024-09-09
  • 深入了解Rust的切片使用

    深入了解Rust的切片使用

    除了引用,Rust 还有另外一种不持有所有权的数据类型:切片(slice),切片允许我们引用集合中某一段连续的元素序列,而不是整个集合。本文让我们来深入了解Rust的切片
    2022-11-11
  • Rust中的函数指针详解

    Rust中的函数指针详解

    Rust是一种现代的系统编程语言,它支持函数指针。函数指针是指向函数的指针,可以将函数作为参数传递给其他函数或存储在变量中。Rust中的函数指针可以用于实现回调函数、动态分发和多态等功能。本文将介绍Rust中的函数指针的基本用法和高级用法。
    2023-05-05
  • Rust 编程语言中的所有权ownership详解

    Rust 编程语言中的所有权ownership详解

    这篇文章主要介绍了Rust 编程语言中的所有权ownership详解的相关资料,需要的朋友可以参考下
    2023-02-02

最新评论