目录
概述 文中列出的这些误解都是我曾经以为正确的概念,我也看到现在仍有许多初学者都因为这些误解而苦苦挣扎。文中的一些术语可能不标准,我列了一张表,以解释我在文中使用某些短语时原本想表达的意思。
短语
解释
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
是互斥集合。下面用几个例子验证这些概念:
1 2 3 4 5 6 7 trait Trait {}impl <T> Trait for T {}impl <T> Trait for &T {} impl <T> Trait for &mut T {}
上面的程序并不能通过编译:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 _`
编译器不允许我们为&T
和&mut T
实现Trait
,因为这会与T
的Trait
实现冲突,后者已经包括了&T
和&mut T
的所有内容。下面的程序可以通过编译,因为&T
和&mut T
互斥:
1 2 3 4 5 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
生命周期,是在一个类似这样的代码示例中:
1 2 3 fn main () { let str_literal: &'static str = "str literal" ; }
他们被告知"str literal"
是被硬编码到编译好的二进制文件中的,并且在运行时被加载到只读存储器中,所以它是不可变的,在整个程序的生命周期中都有效,这就是它'static
的含义。这些观念通过使用static
关键字定义static
变量的规则得到了进一步加强。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
引用,也是可行的,例如:
1 2 3 4 5 6 7 use rand;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
生命周期”。用一个程序来帮助解释这一概念:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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); } } 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
,但反过来就不是这样了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 fn t_ref <'a , T: 'a >(t: &'a T) {}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)); t_bound(string); }
主要结论
T: 'a
比&'a T
更通用也更灵活
T: 'a
接受自有类型、包含引用的自有类型以及引用
&'a T
只接受引用
如果有T: 'static
,那么T: 'a
,因为'static
>= 所有的'a
4) 我的代码没有泛型也没有生命周期 误解的推论
这一令人欣慰的误解之所以能够继续存在,要感谢Rust的生命周期省略规则,它允许我们在函数中省略生命周期注释,因为Rust的借用检查器会按照这些规则来推断生命周期:
对于一个函数,每个输入的引用参数都具有不同的生命周期
如果有且只有一个输入的生命周期,则所有输出的引用都将应用该生命周期
如果有多个输入的生命周期,但其中一个是&self
或&mut self
,则所有输出的引用都将应用self
的生命周期
否则就必须明确输出生命周期
这么多规则很难一下就弄明白,我们来看几个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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 ; fn overlap (s: &str , t: &str ) -> &str ;fn overlap <'a >(s: &'a str , t: &str ) -> &'a str ; fn overlap <'a >(s: &str , t: &'a str ) -> &'a str ; fn overlap <'a >(s: &'a str , t: &'a str ) -> &'a str ; fn overlap (s: &str , t: &str ) -> &'static str ; fn 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程序即使在技术上可以通过编译,但在语义上仍然有可能是错误的。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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实现。它似乎能够正常工作,但如果我们想同时检查几个字节会怎样?
1 2 3 4 5 6 7 8 fn main () { let mut bytes = ByteIter { remainder: b"1123" }; let byte_1 = bytes.next(); let byte_2 = bytes.next(); if byte_1 == byte_2 { } }
呕吼!编译错误:
1 2 3 4 5 6 7 8 9 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
我想我们可以复制每个字节。当我们处理字节时复制当然是可以的,但是如果我们将ByteIter
变成一个通用的切片迭代器,它可以迭代任何&'a [T]
,然后我们可能希望在未来使用它来处理那些复制/克隆成本很高,甚至是不可能复制的类型。好吧,我想我们对此无能为力。所以代码能通过编译,生命周期注释就一定正确吗?
不,当前的生命周期注释实际上是错误的来源!它特别难以发现,因为错误的生命周期注释被省略了。让我们声明省略的生命周期以更清楚地了解问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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专家知道的奇技淫巧:为你的生命周期注释提供描述性名称。让我们再试一次:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
!让我们修复这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 { } }
现在我们回过头来看前一个版本的程序,它错的错误非常明显,那为什么Rust会让它通过编译呢?答案很简单:它是内存安全的。
Rust借用检查器只关心程序中的生命周期注释,它仅使用这些注释来对程序的内存安全性进行静态验证。即使生命周期注释存在语义错误,Rust仍会愉快地编译程序,其结果是使程序受到不必要的限制。
下面是一个与上面相反的简单示例:Rust的生命周期省略规则在这个实例中恰好是语义正确的,但我们无意中编写了一个非常严格的方法,其中包含我们显式声明的不必要的生命周期注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #[derive(Debug)] struct NumRef <'a >(&'a i32 );impl <'a > NumRef<'a > { fn some_method (&'a mut self ) {} } fn main () { let mut num_ref = NumRef(&5 ); num_ref.some_method(); num_ref.some_method(); println! ("{:?}" , num_ref); }
如果有结构体以'a
为泛型,我们几乎不会想编写一个接受参数&'a mut self
的方法。我们想告诉Rust的是“此方法将在结构体的整个生命周期内可变地借用该结构体”。实际上,这意味着Rust的借用检查器最多只允许调用一次some_method
,然后该结构体将成为永久的可变借用,并因此无法再次使用。这种用例非常少见,但困惑的初学者很可能写出上面这种代码,而且还能通过编译。解决方法是不添加不必要的显式生命周期注释,交给Rust的生命周期省略规则处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #[derive(Debug)] struct NumRef <'a >(&'a i32 );impl <'a > NumRef<'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对象的生命周期约束是从上下文中推断出来的 ”。在查看了一些示例之后,我们将意识到生命周期约束的推断是非常直观的,因此我们无需记住上面这些形式化的规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 use std::cell::Ref;trait Trait {}type T1 = Box <dyn Trait>;type T2 = Box <dyn Trait + 'static >;impl dyn Trait {}impl dyn Trait + 'static {}type T3 <'a > = &'a dyn Trait;type T4 <'a > = &'a (dyn Trait + 'a );type T5 <'a > = Ref<'a , dyn Trait>;type 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:
1 2 3 4 5 6 7 8 trait Trait {}struct Struct {}struct Ref <'a , T>(&'a T);impl Trait for Struct {}impl Trait for &Struct {} impl <'a , T> Trait for Ref<'a , T> {}
无论如何,这值得一读,因为当初学者将函数从使用trait对象重构为泛型,或从泛型到使用trait对象时,常常会感到困惑。以这个程序为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 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(); }
这段代码将抛出下面的编译错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 || { | ^^^^^^^^^^^^^^^^^^
很好,编译期已经告诉我们如何修复问题了:
1 2 3 4 5 6 7 8 9 10 11 12 13 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编译器实际看到的:
1 2 3 4 5 6 7 8 9 10 11 12 13 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比我更了解我程序的语义
这种误解结合了前面两种误解,例如:
1 2 3 4 5 use std::fmt::Display;fn box_displayable <T: Display>(t: T) -> Box <dyn Display> { Box ::new(t) }
抛出的错误为:
1 2 3 4 5 6 7 8 9 10 11 12 13 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
,而它推荐的修复方式就基于这个未声明的事实,虽然编译器没有告诉我们,但无需介意:
1 2 3 4 5 use std::fmt::Display;fn box_displayable <T: Display + 'static >(t: T) -> Box <dyn Display> { Box ::new(t) }
所以程序现在可以通过编译了……但这真的是我们想要的吗?可能是也可能不是。编译器没有提到其他修复,但下面这种也是合适的:
1 2 3 4 5 use std::fmt::Display;fn box_displayable <'a , T: Display + 'a >(t: T) -> Box <dyn Display + 'a > { Box ::new(t) }
此函数接受参数的范围比前面的版本更广!但这会让它变得更好用吗?不一定,要看我们程序的要求和约束。这个例子有点抽象,所以让我们看一个更简单且明显的例子:
1 2 3 fn return_first (a: &str , b: &str ) -> &str { a }
抛出的错误为:
1 2 3 4 5 6 7 8 9 10 11 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 { | ^^^^ ^^^^^^^ ^^^^^^^ ^^^
错误消息建议使用相同的生命周期注释输入和输出。如果我们这样做,程序的确会通过编译,但这个函数会过度限制返回类型。我们真正想要的是这样的:
1 2 3 fn return_first <'a >(a: &'a str , b: &str ) -> &'a str { a }
主要结论
Rust对trait对象的生命周期省略规则并不总是适用于所有情况
Rust并不比你更了解你程序的语义
Rust编译器的错误消息修复建议能够使你的程序通过编译,但这不一定是使你的程序通过编译的最合适的修复方式
8) 生命周期可以在运行时增长和收缩 误解的推论
容器类型可以在运行时切换引用,以改变它们的生命周期
Rust借用检查器会对程序进行高级控制流分析
这不能通过编译:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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" ); has.lifetime = &short; assert_eq! (has.lifetime, "short" ); has.lifetime = &long; assert_eq! (has.lifetime, "long" ); } assert_eq! (has.lifetime, "long" ); }
这段代码抛出:
1 2 3 4 5 6 7 8 9 10 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
下面这段代码也不能通过编译,且与上面的代码抛出的错误完全一致:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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" ); has.lifetime = &short; assert_eq! (has.lifetime, "short" ); has.lifetime = &long; assert_eq! (has.lifetime, "long" ); } assert_eq! (has.lifetime, "long" ); }
Rust中的生命周期必须在编译时静态验证,借用检查器只会进行最基本的控制流分析,所以它假设if-else
语句中的每个块和match
语句中的每个匹配分支都可以被执行,然后为变量选择可能的最短的生命周期。一旦一个变量被一个生命周期约束,就将永远 受到该生命周期的约束。变量的生命周期只能收缩,而所有的收缩都是在编译时决定的。
主要结论
生命周期会在编译期静态验证
生命周期不能在运行时以任何方式增长、收缩或改变
Rust借用检查器将始终为变量选择尽可能短的生命周期,并会假设所有分支代码都能够被执行
9) 将可变引用降级为共享引用是安全的 误解的推论
重新借用一个引用将结束现有的生命周期并开始一个新的生命周期
您可以将可变引用传给期望共享引用的函数,因为Rust会隐式地重新借用这个可变引用并将其变为不可变的共享引用:
1 2 3 4 5 6 7 fn takes_shared_ref (n: &i32 ) {}fn main () { let mut a = 10 ; takes_shared_ref(&mut a); takes_shared_ref(&*(&mut a)); }
直觉上这是有道理的,因为将一个可变引用重新借用为不可变的应该不会造成什么影响,对吧?令人惊讶的是,实际上并非没有影响,来看下面的程序就无法通过编译:
1 2 3 4 5 6 fn main () { let mut a = 10 ; let b: &i32 = &*(&mut a); let c: &i32 = &a; dbg!(b, c); }
这段代码抛出如下错误:
1 2 3 4 5 6 7 8 9 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
我们确实进行了可变借用,但它会立即无条件地重新进行不可变借用,然后被丢弃。为什么Rust将不可变的重新借用,仍然视为可变借用的独占生命周期?虽然在上面的特定示例中没有问题,但允许将可变引用降级为共享引用的功能,确实会引入潜在的内存安全问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 use std::sync::Mutex;struct Struct { mutex: Mutex<String > } impl Struct { fn get_string (&mut self ) -> &str { self .mutex.get_mut().unwrap() } fn mutate_string (&self ) { *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(); dbg!(str_ref); }
这里的要点是,当你将可变引用重新借用为共享引用时,你不只得到了共享引用还掉进了一个大坑:重新借用还延长了可变引用的生命周期,即使可变引用自身已被析构。使用重新借用的共享引用非常难,因为它不仅不可变,还不能被其他任何共享引用共享。重新借用的共享引用集合了可变引用和共享引用的所有缺点,且并没有得到两者的任一的优点。我认为将可变引用重新借用为共享引用应该被视为Rust反面模式。意识到这种反面模式很重要,这样当您看到这样的代码时就可以很容易地发现它:
1 2 3 4 5 6 7 8 9 10 11 12 fn some_function <T>(some_arg: &mut T) -> &T;struct Struct ;impl Struct { fn some_method (&mut self ) -> &Self ; fn other_method (&mut self ) -> &T; }
即使你避免了在函数和方法签名中的重新借用,Rust仍然会自动进行隐式重新借用,所以我们很容易在没有意识到的情况下遇到这种问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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(); dbg!(player_a, player_b); }
上面的代码无法通过编译。or_default()
将返回一个&mut Player
,我们的显式类型注释将使Rust隐式地重新借用为&Player
。所以,正确的写法应该是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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>) { server.entry(player_a).or_default(); server.entry(player_b).or_default(); let player_a = server.get(&player_a); let player_b = server.get(&player_b); dbg!(player_a, player_b); }
这么写有点奇怪还很笨拙,但这是我们在内存安全问题上必要的牺牲。
主要结论
尽量不要将可变引用重新借用为共享引用,否则你可能会遇到大麻烦
对可变引用进行重新借用并不会结束它的生命周期,即使该引用已被删除
10) 闭包遵循与函数相同的生命周期省略规则 这与其说是一种误解,不如说是Rust的陷阱。
尽管闭包也是函数,但并不遵循与函数相同的生命周期省略规则。
1 2 3 4 5 6 7 fn function (x: &i32 ) -> &i32 { x } fn main () { let closure = |x: &i32 | x; }
报错:
1 2 3 4 5 6 7 8 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`
补全上面代码省略的内容:
1 2 3 4 5 6 7 8 9 10 fn function <'a >(x: &'a i32 ) -> &'a i32 { x } fn main () { let closure = for <'a , 'b > |x: &'a i32 | -> &'b i32 { x }; }
这种差异没有什么合理的解释。闭包最初是使用与函数不同的类型推断语义实现的,不过我们可能要永远这么用下去了,因为如果现在统一两者的实现将是一个破坏性的更新。那么如何显式地注释闭包呢?我们的选择包括:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 fn main () { 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 }; 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借用检查器将始终为变量选择尽可能短的生命周期,并会假设所有分支代码都能够被执行
尽量不要将可变引用重新借用为共享引用,否则你可能会遇到大麻烦
对可变引用进行重新借用并不会结束它的生命周期,即使该引用已被删除
每个编程语言都有些陷阱🤷
讨论 讨论这篇文章的地方:
订阅 订阅下一篇文章的地方:
深入阅读