NOTE本文内容大多翻译自原文:Common Rust Lifetime Misconceptions。
目录
概述
文中列出的这些误解都是我曾经以为正确的概念,我也看到现在仍有许多初学者都因为这些误解而苦苦挣扎。文中的一些术语可能不标准,我列了一张表,以解释我在文中使用某些短语时原本想表达的意思。
| 短语 | 解释 |
|---|---|
T | 1) 包含所有可能类型的集合 或 2) 该集合中的某种类型 |
| 自有类型 | 某些非引用类型,如i32、String、Vec等 |
| 1) 借用类型 或 2) 引用类型 | 某些引用类型,可变或不可变均可,如&i32、&mut i32等 |
| 1) 可变借用 或 2) 互斥借用 | 排他可变引用,如&mut T |
| 1) immut ref or 2) shared ref | shared immutable reference, i.e. &T |
概念误解
总的来说,一个变量的生命周期是指:该变量指向的数据在其当前的内存地址中,能被编译器静态验证为有效的时间。下面将用6500字的篇幅来详细说明大家通常会混淆的地方。
1) T只包含自有类型
这种误解更多的是关于泛型,而是不关于生命周期,但是泛型和生命周期在Rust中紧密的交织在一起,所以不可能在谈论一个的时候不提及另一个。总之:
当我刚开始学习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是互斥集合。下面用几个例子验证这些概念:
trait Trait {}
impl<T> Trait for T {}
impl<T> Trait for &T {} // ❌
impl<T> Trait for &mut T {} // ❌上面的程序并不能通过编译:
error[E0119]: conflicting implementations of trait `Trait` for type `&_`: --> src/lib.rs:5:1 |3 | impl<T> Trait for T {} | ------------------- first implementation here4 |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 _`编译器不允许我们为&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 {} // ✅主要结论
T是&T和&mut T的超集&T和&mut T是互斥集合
2) 如果T: 'static,则T在程序整个生命周期都有效
误解的推论
T: 'static应该读作“T具有'static的生命周期”&'static T与T: 'static是一回事- 如果有
T: 'static,则T一定是不可变的 - 如果有
T: 'static,则T只能在编译期创建
大多数Rust初学者第一次接触到'static生命周期,是在一个类似这样的代码示例中:
fn main() { let str_literal: &'static str = "str literal";}他们被告知"str literal"是被硬编码到编译好的二进制文件中的,并且在运行时被加载到只读存储器中,所以它是不可变的,在整个程序的生命周期中都有效,这就是它'static的含义。这些观念通过使用static关键字定义static变量的规则得到了进一步加强。
// 注意:这个理智纯粹是为了说明问题。// 永远不要使用`static mut`这种别扭的变量。// 在Rust中,有一些安全的全局可变单例模式,// 但这些不在本文的讨论范围之内。
static BYTES: [u8; 3] = [1, 2, 3];static mut MUT_BYTES: [u8; 3] = [1, 2, 3];
fn main() { MUT_BYTES[0] = 99; // ❌ - 修改静态变量是非安全的
unsafe { MUT_BYTES[0] = 99; assert_eq!(99, MUT_BYTES[0]); }}关于static变量
- 只能在编译期期创建
- 应该是不可变的,修改静态变量是非安全的
- 在整个程序的生命周期中都有效
'static生命周期可能是根据static变量的默认生命周期来命名的,对吧?所以'static生命周期遵循static变量相同的规则是很合理的,对吧?
也对,但是具有'static生命周期的类型与受到'static生命周期约束的类型是不同的。后者可以在运行时动态分配、可以安全的在任何地方修改、可以被析构,而且其生命周期可以持续任意的时间。
在这一点上,将&'static T与T: 'static区分开很重要。
&'static T是对某个T的不可变引用,可以安全的持有无限长时间,包括持有到程序结束。但这只有在T本身是不可变的且在引用被创建后没有移动时才可以。T并不一定要在编译期创建。以内存泄漏为代价,在运行时生成随机的动态分配的数据再返回其'static引用,也是可行的,例如:
use rand;
// 在运行时生成随机的'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是一种可以安全的无限期持有的T,包括持有到程序结束。T: 'static包括所有的&'static T,此外还包括所有的自有类型,比如String、Vec等。一些数据的持有者被保证,只要所有者持有数据,数据就不会失效,因此所有者可以安全地无限期地持有数据,包括持有到程序结束。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() { // 所以字符串都是随机生成且在运行时动态分配的 let string = rand::random::<u64>().to_string(); strings.push(string); } }
// 字符串为自有类型,因此受到'static约束 for mut string in strings { // 所有字符串均可变 string.push_str("a mutation"); // 所有字符串均可析构 drop_static(string); }
// 所有字符串在程序结束前均已失效 println!("I am the end of program");}主要结论
T: 'static应读作“T受到'static生命周期约束”- 如果有
T: 'static,则T可以是一个具有'static生命周期的借用类型 或 一个自有类型 - 因为
T: 'static包括自有类型,这意味着T:- 可以在运行时被动态的分配
- 不需要在程序的生命周期中始终有效
- 可以安全、自由的修改
- 可以在运行时动态的析构
- 可以有不同持续时间的生命周期
3) &'a T与T: 'a是一回事
这条误解是上一条误解的泛化版本。
&'a T要求并暗指T: 'a,因为如果T本身在生命周期'a上是无效的,那么以'a为生命周期的T的引用在生命周期'a上自然也是无效的。例如,Rust编译器不允许构造&'static Ref<'a, T>类型,因为如果Ref只在'a上有效,我们就不能对它使用'static引用。
T: 'a包括所有的&'a T,但反过来就不是这样了。
// 只接受受到'a约束的类型的引用fn t_ref<'a, T: 'a>(t: &'a T) {}
// 接受任意受到'a约束的类型fn t_bound<'a, T: 'a>(t: T) {}
// 包含一个引用的自有类型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)); // ❌ - 期待收到一个引用,但实际收到一个结构体 t_ref(&Ref(&string)); // ✅
// string var is bounded by 'static which is bounded by 'a // 字符串变量受到'static约束,即受到'a约束 t_bound(string); // ✅}主要结论
T: 'a比&'a T更通用也更灵活T: 'a接受自有类型、包含引用的自有类型以及引用&'a T只接受引用- 如果有
T: 'static,那么T: 'a,因为'static>= 所有的'a
4) 我的代码没有泛型也没有生命周期
误解的推论
- 可以避免使用泛型和生命周期
这一令人欣慰的误解之所以能够继续存在,要感谢Rust的生命周期省略规则,它允许我们在函数中省略生命周期注释,因为Rust的借用检查器会按照这些规则来推断生命周期:
- 对于一个函数,每个输入的引用参数都具有不同的生命周期
- 如果有且只有一个输入的生命周期,则所有输出的引用都将应用该生命周期
- 如果有多个输入的生命周期,但其中一个是
&self或&mut self,则所有输出的引用都将应用self的生命周期 - 否则就必须明确输出生命周期
这么多规则很难一下就弄明白,我们来看几个例子:
// 省略fn print(s: &str);
// 展开为fn print<'a>(s: &'a str);
// 省略fn trim(s: &str) -> &str;
// 展开为fn trim<'a>(s: &'a str) -> &'a str;
// 错误,因为没有输入,无法确定输出的生命周期fn get_str() -> &str;
// 展开的选项包括fn get_str<'a>() -> &'a str; // 泛型版本fn get_str() -> &'static str; // 'static版本
// 错误,因为有多个输入,无法确定输出的生命周期fn overlap(s: &str, t: &str) -> &str;
// 展开(依然有部分省略)的选项包括fn overlap<'a>(s: &'a str, t: &str) -> &'a str; // 输出的有效期不能超过sfn overlap<'a>(s: &str, t: &'a str) -> &'a str; // 输出的有效期不能超过tfn overlap<'a>(s: &'a str, t: &'a str) -> &'a str; // 输出的有效期不能超过s和tfn overlap(s: &str, t: &str) -> &'static str; // 输出的有效期可以超过s和tfn overlap<'a>(s: &str, t: &str) -> &'a str; // 输出的生命周期与输入无关
// 展开为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;
// 省略fn compare(&self, s: &str) -> &str;
// 展开为fn compare<'a, 'b>(&'a self, &'b str) -> &'a str;如果你曾写过
- 一个结构体方法
- 一个接受多个引用的函数
- 一个返回多个引用的函数
- 一个泛型函数
- 一个trait对象(后文会有更多介绍)
- 一个闭包(后文会有更多介绍)
那么你的代码中就充满了省略的生命周期注释。
主要结论
- 几乎所有的Rust代码都是泛型代码,到处都有被省略的生命周期注释。
5) 如果代码通过编译,就说明我们的生命周期注释是正确的
误解的推论
- Rust函数的生命周期省略规则总是正确
- Rust的借用检查器在技术上和语义上总是正确
- Rust比我更了解我的程序的语义
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是一个迭代字节片的迭代器。为了简洁起见,我们跳过了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 }}呕吼!编译错误:
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 here20 | let byte_2 = bytes.next(); | ^^^^^ second mutable borrow occurs here21 | if byte_1 == byte_2 { | ------ first borrow later used here我想我们可以复制每个字节。当我们处理字节时复制当然是可以的,但是如果我们将ByteIter变成一个通用的切片迭代器,它可以迭代任何&'a [T],然后我们可能希望在未来使用它来处理那些复制/克隆成本很高,甚至是不可能复制的类型。好吧,我想我们对此无能为力。所以代码能通过编译,生命周期注释就一定正确吗?
不,当前的生命周期注释实际上是错误的来源!它特别难以发现,因为错误的生命周期注释被省略了。让我们声明省略的生命周期以更清楚地了解问题:
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) } }}这并没有帮助,我们依然很困惑。这里有一个只有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) } }}每个返回的字节都注释为'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(); std::mem::drop(bytes); // 我们甚至可以在此处析构迭代器! if byte_1 == byte_2 { // ✅ // do something }}现在我们回过头来看前一个版本的程序,它错的错误非常明显,那为什么Rust会让它通过编译呢?答案很简单:它是内存安全的。
Rust借用检查器只关心程序中的生命周期注释,它仅使用这些注释来对程序的内存安全性进行静态验证。即使生命周期注释存在语义错误,Rust仍会愉快地编译程序,其结果是使程序受到不必要的限制。
下面是一个与上面相反的简单示例:Rust的生命周期省略规则在这个实例中恰好是语义正确的,但我们无意中编写了一个非常严格的方法,其中包含我们显式声明的不必要的生命周期注释。
#[derive(Debug)]struct NumRef<'a>(&'a i32);
impl<'a> NumRef<'a> { // 我们的结构体以为'a泛型,这是否意味着我需要将self参数也注释为'a? // (答案:并不是) fn some_method(&'a mut self) {}}
fn main() { let mut num_ref = NumRef(&5); num_ref.some_method(); // 可变借用num_ref直到其生命周期结束 num_ref.some_method(); // ❌ println!("{:?}", num_ref); // ❌}如果有结构体以'a为泛型,我们几乎不会想编写一个接受参数&'a mut self的方法。我们想告诉Rust的是“此方法将在结构体的整个生命周期内可变地借用该结构体”。实际上,这意味着Rust的借用检查器最多只允许调用一次some_method,然后该结构体将成为永久的可变借用,并因此无法再次使用。这种用例非常少见,但困惑的初学者很可能写出上面这种代码,而且还能通过编译。解决方法是不添加不必要的显式生命周期注释,交给Rust的生命周期省略规则处理:
#[derive(Debug)]struct NumRef<'a>(&'a i32);
impl<'a> NumRef<'a> { // 不再给mut self添加'a注释 fn some_method(&mut self) {}
// 上面的方法实际上是: 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); // ✅}主要结论
- Rust函数的生命周期省略规则并不适用于所有情况
- Rust并不比你更了解你程序的语义
- 给你的生命周期注解起描述性的名字
- 注意显式生命生命周期注释的位置以及原因
6) box指针中的trait对象没有生命周期概念
之前我们讨论了Rust的函数生命周期省略规则,当然,Rust也有针对特征对象的省略规则,它们是:
- 如果一个trait对象被用作泛型类型的类型参数,那么它的生命周期约束是从包含的类型中推断出来的
- 如果包含的类型有唯一的生命周期约束,那么就使用它
- 如果包含的类型有多个生命周期约束,那么必须指定一个显式生命周期约束
- 如果以上均不适用,则
- 如果该trait由唯一的一个生命周期约束定义,则使用该约束
- 如果
'static用于任何生命周期约束,则使用'static - 如果trait没有生命周期约束,那么它的生命周期是通过表达式推断出来的,并且在表达式之外是
'static的
虽然上面这些规则听起来非常复杂,但是可以简单的概括为“trait对象的生命周期约束是从上下文中推断出来的”。在查看了一些示例之后,我们将意识到生命周期约束的推断是非常直观的,因此我们无需记住上面这些形式化的规则:
use std::cell::Ref;
trait Trait {}
// 省略type T1 = Box<dyn Trait>;//展开,Box<T>对T没有生命周期约束,因此推断为'statictype T2 = Box<dyn Trait + 'static>;
// 省略impl dyn Trait {}// 展开为impl dyn Trait + 'static {}
// 省略type T3<'a> = &'a dyn Trait;// 展开,&'a T 要求 T: 'a,因此推断为'atype T4<'a> = &'a (dyn Trait + 'a);
// 省略type T5<'a> = Ref<'a, dyn Trait>;// 展开,Ref<'a, T> 要求 T: 'a,因此推断为'atype T6<'a> = Ref<'a, dyn Trait + 'a>;
trait GenericTrait<'a>: 'a {}
// 省略type T7<'a> = Box<dyn GenericTrait<'a>>;// 展开为type T8<'a> = Box<dyn GenericTrait<'a> + 'a>;
// 省略impl<'a> dyn GenericTrait<'a> {}// 展开为impl<'a> dyn GenericTrait<'a> + 'a {}实现trait的具体类型可以有引用,因此它们也有生命周期约束,所以它们对应的trait对象也具有生命周期约束。你也可以直接为显然具有生命周期约束的引用实现trait:
trait Trait {}
struct Struct {}struct Ref<'a, T>(&'a T);
impl Trait for Struct {}impl Trait for &Struct {} // 直接为引用类型实现Traitimpl<'a, T> Trait for Ref<'a, T> {} // 为包含引用的类型实现无论如何,这值得一读,因为当初学者将函数从使用trait对象重构为泛型,或从泛型到使用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();}这段代码将抛出下面的编译错误:
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 || { | ^^^^^^^^^^^^^^^^^^很好,编译期已经告诉我们如何修复问题了:
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();}现在可以通过编译了,不过这两个函数放在一起看起来很奇怪,为什么第二个函数需要'static约束的T,而第一个函数就不需要?这是一个棘手的问题。应用生命周期省略规则,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();}主要结论
- 所有trait对象都具有某些推断的默认生命周期约束
7) 编译器的错误提示会告诉我如何修复程序
误解的推论
- Rust对trait对象的生命周期省略规则总是正确的
- Rust比我更了解我程序的语义
这种误解结合了前面两种误解,例如:
use std::fmt::Display;
fn box_displayable<T: Display>(t: T) -> Box<dyn Display> { // ❌ Box::new(t)}抛出的错误为:
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) | ^^^^^^^^^^^让我们按照编译器告诉我们方式来修复问题,它将我们boxed trait的约束自动推断为'static,而它推荐的修复方式就基于这个未声明的事实,虽然编译器没有告诉我们,但无需介意:
use std::fmt::Display;
fn box_displayable<T: Display + 'static>(t: T) -> Box<dyn Display> { // ✅ Box::new(t)}所以程序现在可以通过编译了……但这真的是我们想要的吗?可能是也可能不是。编译器没有提到其他修复,但下面这种也是合适的:
use std::fmt::Display;
fn box_displayable<'a, T: Display + 'a>(t: T) -> Box<dyn Display + 'a> { // ✅ Box::new(t)}此函数接受参数的范围比前面的版本更广!但这会让它变得更好用吗?不一定,要看我们程序的要求和约束。这个例子有点抽象,所以让我们看一个更简单且明显的例子:
fn return_first(a: &str, b: &str) -> &str { // ❌ a}抛出的错误为:
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 { | ^^^^ ^^^^^^^ ^^^^^^^ ^^^错误消息建议使用相同的生命周期注释输入和输出。如果我们这样做,程序的确会通过编译,但这个函数会过度限制返回类型。我们真正想要的是这样的:
fn return_first<'a>(a: &'a str, b: &str) -> &'a str { // ✅ a}主要结论
- Rust对trait对象的生命周期省略规则并不总是适用于所有情况
- Rust并不比你更了解你程序的语义
- Rust编译器的错误消息修复建议能够使你的程序通过编译,但这不一定是使你的程序通过编译的最合适的修复方式
8) 生命周期可以在运行时增长和收缩
误解的推论
- 容器类型可以在运行时切换引用,以改变它们的生命周期
- Rust借用检查器会对程序进行高级控制流分析
这不能通过编译:
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"); // “切换”到short生命周期 has.lifetime = &short; assert_eq!(has.lifetime, "short");
// “切换回”long生命周期(但实际上并没有) has.lifetime = &long; assert_eq!(has.lifetime, "long"); // `short`在此处析构 }
assert_eq!(has.lifetime, "long"); // ❌ - `short`在析构之后仍“被借用”}这段代码抛出:
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 borrowed16 | assert_eq!(has.lifetime, "long"); | --------------------------------- borrow later used here下面这段代码也不能通过编译,且与上面的代码抛出的错误完全一致:
struct Has<'lifetime> { lifetime: &'lifetime str,}
fn main() { let long = String::from("long"); let mut has = Has { lifetime: &long }; assert_eq!(has.lifetime, "long");
// 虽然这个代码块从不执行 if false { let short = String::from("short"); // “切换”到short生命周期 has.lifetime = &short; assert_eq!(has.lifetime, "short");
// “切换回”long生命周期(但实际上并没有) has.lifetime = &long; assert_eq!(has.lifetime, "long"); // `short`在此处析构 }
assert_eq!(has.lifetime, "long"); // ❌ - `short`在析构之后仍“被借用”}Rust中的生命周期必须在编译时静态验证,借用检查器只会进行最基本的控制流分析,所以它假设if-else语句中的每个块和match语句中的每个匹配分支都可以被执行,然后为变量选择可能的最短的生命周期。一旦一个变量被一个生命周期约束,就将永远受到该生命周期的约束。变量的生命周期只能收缩,而所有的收缩都是在编译时决定的。
主要结论
- 生命周期会在编译期静态验证
- 生命周期不能在运行时以任何方式增长、收缩或改变
- Rust借用检查器将始终为变量选择尽可能短的生命周期,并会假设所有分支代码都能够被执行
9) 将可变引用降级为共享引用是安全的
误解的推论
- 重新借用一个引用将结束现有的生命周期并开始一个新的生命周期
您可以将可变引用传给期望共享引用的函数,因为Rust会隐式地重新借用这个可变引用并将其变为不可变的共享引用:
fn takes_shared_ref(n: &i32) {}
fn main() { let mut a = 10; takes_shared_ref(&mut a); // ✅ takes_shared_ref(&*(&mut a)); // 上一行实际上执行的代码}直觉上这是有道理的,因为将一个可变引用重新借用为不可变的应该不会造成什么影响,对吧?令人惊讶的是,实际上并非没有影响,来看下面的程序就无法通过编译:
fn main() { let mut a = 10; let b: &i32 = &*(&mut a); // re-borrowed as immutable let c: &i32 = &a; dbg!(b, c); // ❌}这段代码抛出如下错误:
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 here4 | let c: &i32 = &a; | ^^ immutable borrow occurs here5 | dbg!(b, c); | - mutable borrow later used here我们确实进行了可变借用,但它会立即无条件地重新进行不可变借用,然后被丢弃。为什么Rust将不可变的重新借用,仍然视为可变借用的独占生命周期?虽然在上面的特定示例中没有问题,但允许将可变引用降级为共享引用的功能,确实会引入潜在的内存安全问题:
use std::sync::Mutex;
struct Struct { mutex: Mutex<String>}
impl Struct { // 将可变self降级为共享str fn get_string(&mut self) -> &str { self.mutex.get_mut().unwrap() } fn mutate_string(&self) { // 如果Rust允许将可变引用降级为共享引用 // 则下面这行代码将使从get_string方法返回的任何共享引用无效 *self.mutex.lock().unwrap() = "surprise!".to_owned(); }}
fn main() { let mut s = Struct { mutex: Mutex::new("string".to_owned()) }; let str_ref = s.get_string(); // 可变引用降级为共享引用 s.mutate_string(); // str_ref将变得无效,产生悬空指针 dbg!(str_ref); // ❌ - 如我们所料}这里的要点是,当你将可变引用重新借用为共享引用时,你不只得到了共享引用还掉进了一个大坑:重新借用还延长了可变引用的生命周期,即使可变引用自身已被析构。使用重新借用的共享引用非常难,因为它不仅不可变,还不能被其他任何共享引用共享。重新借用的共享引用集合了可变引用和共享引用的所有缺点,且并没有得到两者的任一的优点。我认为将可变引用重新借用为共享引用应该被视为Rust反面模式。意识到这种反面模式很重要,这样当您看到这样的代码时就可以很容易地发现它:
// 将可变T降级为共享Tfn some_function<T>(some_arg: &mut T) -> &T;
struct Struct;
impl Struct { // 将可变self降级为共享self fn some_method(&mut self) -> &Self;
// 将可变self降级为共享T fn other_method(&mut self) -> &T;}即使你避免了在函数和方法签名中的重新借用,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>) { // 从服务器获取玩家,如果玩家不存在则创建或插入新玩家 let player_a: &Player = server.entry(player_a).or_default(); let player_b: &Player = server.entry(player_b).or_default();
// do something with players dbg!(player_a, player_b); // ❌}上面的代码无法通过编译。or_default()将返回一个&mut Player,我们的显式类型注释将使Rust隐式地重新借用为&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>) { // 丢弃返回的可变Player引用,反正我们也不能同时使用它们 server.entry(player_a).or_default(); server.entry(player_b).or_default();
// 重新获取players,这次直接获取其不可变引用,且不带有啊任何隐式重新借用 let player_a = server.get(&player_a); let player_b = server.get(&player_b);
// do something with players dbg!(player_a, player_b); // ✅}这么写有点奇怪还很笨拙,但这是我们在内存安全问题上必要的牺牲。
主要结论
- 尽量不要将可变引用重新借用为共享引用,否则你可能会遇到大麻烦
- 对可变引用进行重新借用并不会结束它的生命周期,即使该引用已被删除
10) 闭包遵循与函数相同的生命周期省略规则
这与其说是一种误解,不如说是Rust的陷阱。
尽管闭包也是函数,但并不遵循与函数相同的生命周期省略规则。
fn function(x: &i32) -> &i32 { x}
fn main() { let closure = |x: &i32| x; // ❌}报错:
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`补全上面代码省略的内容:
// 输入的生命周期应用于输出的生命周期fn function<'a>(x: &'a i32) -> &'a i32 { x}
fn main() { // 输入和输出分别得到了各自不同的生命周期 let closure = for<'a, 'b> |x: &'a i32| -> &'b i32 { x }; // 注意:上面一行代码的语法并不合法,我们只是使用它说明情况}这种差异没有什么合理的解释。闭包最初是使用与函数不同的类型推断语义实现的,不过我们可能要永远这么用下去了,因为如果现在统一两者的实现将是一个破坏性的更新。那么如何显式地注释闭包呢?我们的选择包括:
fn main() { // 若转换为trait对象,占用空间会变得固定,于是产生编译错误 let identity: dyn Fn(&i32) -> &i32 = |x: &i32| x;
// 可以分配在堆上,但感觉很笨重 let identity: Box<dyn Fn(&i32) -> &i32> = Box::new(|x: &i32| x);
// 可以跳过分配,只创建一个静态引用 let identity: &dyn Fn(&i32) -> &i32 = &|x: &i32| x;
// 上面这一行实际上是: let identity: &'static (dyn for<'a> Fn(&'a i32) -> &'a i32 + 'static) = &|x: &i32| -> &i32 { x };
// 这么写是最理想的,但这样的语法是无效的 let identity: impl Fn(&i32) -> &i32 = |x: &i32| x;
// 这样写也很理想,但依然是无效语法 let identity = for<'a> |x: &'a i32| -> &'a i32 { x };
// `impl trait`只能用在在函数签名的返回值部分 fn return_identity() -> impl Fn(&i32) -> &i32 { |x| x } let identity = return_identity();
// 上面版本的泛型化写法 fn annotate<T, F>(f: F) -> F where F: Fn(&T) -> &T { f } let identity = annotate(|x: &i32| x);}你应该已经从上面的示例中注意到了,当闭包类型用作trait约束时,它们确实应用了通常的函数生命周期省略规则。
此处并没有什么教训或启发,Rust现在就是这样。
主要结论
- 每个编程语言都有些陷阱🤷
结论
T是&T和&mut T的超集&T和&mut T是互斥集合T: 'static应读作“T受到'static生命周期约束”- 如果有
T: 'static,则T可以是一个具有'static生命周期的借用类型 或 一个自有类型 - 因为
T: 'static包括自有类型,这意味着T:- 可以在运行时被动态的分配
- 不需要在程序的生命周期中始终有效
- 可以安全、自由的修改
- 可以在运行时动态的析构
- 可以有不同持续时间的生命周期
T: 'a比&'a T更通用也更灵活T: 'a接受自有类型、包含引用的自有类型以及引用&'a T只接受引用- 如果有
T: 'static,那么T: 'a,因为'static>= 所有的'a - 几乎所有的Rust代码都是泛型代码,到处都有被省略的生命周期注释。
- Rust函数的生命周期省略规则并不适用于所有情况
- Rust并不比你更了解你程序的语义
- 给你的生命周期注解起描述性的名字
- 注意显式生命生命周期注释的位置以及原因
- 所有trait对象都具有某些推断的默认生命周期约束
- Rust编译器的错误消息修复建议能够使你的程序通过编译,但这不一定是使你的程序通过编译的最合适的修复方式
- 生命周期会在编译期静态验证
- 生命周期不能在运行时以任何方式增长、收缩或改变
- Rust借用检查器将始终为变量选择尽可能短的生命周期,并会假设所有分支代码都能够被执行
- 尽量不要将可变引用重新借用为共享引用,否则你可能会遇到大麻烦
- 对可变引用进行重新借用并不会结束它的生命周期,即使该引用已被删除
- 每个编程语言都有些陷阱🤷
讨论
讨论这篇文章的地方:
订阅
订阅下一篇文章的地方:
- Following pretzelhammer on Twitter or
- Subscribing to this repo’s release RSS feed or
- 关注本repo的release (click
Watch-> clickCustom-> selectReleases-> clickApply)