生命周期误解的意译版 + 个人批注
前言
原文链接
为什么要翻译:强制放慢阅读速度,加深对内容的理解;分享自己的一些理解和总结
如何翻译的:主要是意译,有意将长句子拆开,便于阅读,同时加入了一些承上启下的句子,补充更容易理解的行文逻辑。另外还添加了一些结论性的注释,论据主要来自于Ownership - The Rustonomicon ,作为不同视角的参考。
应该如何阅读:时刻参照原文 。根据我读译文的经验,不能过分相信翻译者!当读起来不顺畅,有异样感觉时,就看原文吧,可能找到导向更好理解的线索。
引言 本文所列出的对生命周期的误解,我过去都经历过,而如今,我发现很多初学者依然身陷其中。
可能我所用的术语并不标准,因此列出下表,阐明我将使用的词语,以及对应的含义
短语
含义
T
1. 一个集合,包含所有可能的类型 2. 在该集合中的某个类型
所有权类型
非引用的类型,比如i32
,String
,Vec
等
1. 借用类型 或者 2. 引用类型
无关可变性的引用类型,比如&i32
, &mut i32
等
1. 可变引用(借用) 或者 2. 排它引用(借用)
具有排它性、唯一性的可变引用,比如&mut T
1. 不可变引用 或者 2. 共享引用
共享的不可变引用,比如&T
误解 概括一下,一个变量的生命周期是指,变量所指向的内存数据,在其当前内存地址上可以保持多长的有效性,该有效性由编译器静态验证。
Rutonomicon定义 :Lifetimes are named regions of code that a reference must be valid for. 有名称的代码作用域,在作用域中引用必须保证有效
1) T
只包含所有权类型 相比生命周期,泛型可能是更适合这个误解的主题。但在Rust世界中,泛型和生命周期关联十分紧密,几乎不能单独谈论。
当我开始学习Rust时,我能够理解 i32
, &i32
, 和&mut i32
属于不同的类型,同时我也理解,泛型T
代表着一个类型集合。尽管分开理解没有障碍,但放在一起时就有了问题。在早期,我对Rust泛型的理解是这样的:
Type Variable
T
&T
&mut T
Examples
i32
,Vec<T>
&i32
,&Vec<T>
&mut i32
, &mut Vec<T>
T
是所有权类型集合,&T
是不可变借用集合,&mut T
则是可变借用集合, T
, &T
, &mut T
是三个不相交的有限集合。简洁,清晰,容易,符合直觉,但是,完全错了。实际情况是这样的
Type Variable
T
&T
&mut T
Examples
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
和&mut T
确实是不相交的。
下面是几个解释相关概念的样例:
trait Trait {}impl <T> Trait for T {}impl <T> Trait for &T {} impl <T> Trait for &mut T {}
编译器并不会允许我们为&T
和&mut T
实现Trait
,因为和为T
实现的Trait
冲突了,正如之前所说,T
包含所有的&T
和&mut T
。而如下的程序则可以编译,因为&T
和&mut T
不相交。
trait Trait {}impl <T> Trait for &T {} impl <T> Trait for &mut T {}
关键点
T
是&T
和&mut
的超集
&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 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 ]); } }
静态变量有如下性质:
只在编译期被创建
默认不可变,修改需要unsafe
对整个程序范围有效
所以'static
生命周期标记,就是以静态变量默认生命周期命名的,对吧?
所以如果我们断言'static
生命周期也遵守上述静态变量的规则,也理所应当,对吧?
是的,这没错。但是,我们要区分:
一个变量拥有 'static
生命周期
具体例子是什么?就是静态变量本身吗?
一个变量被'static
生命周期绑定
这个应该指T: 'static
后者实际上可以在运行时分配内存并创建,也能在safe Rust中自由访问修改,甚至被drop,在任意长度存活。
其实这里我非常很困惑,原文如下:
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?
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.
这个Well, yes
具体肯定的是什么东西?’static
lifetime has to follow 一句中'static lifetime
是主语,这个主语怎么实施follow这个动作?凡是出现‘static
,不都是被绑定的情况吗,with 'static
是什么意思体现在哪里呢?
由此我们再区分一个重要的点: &'static T
和 T: 'static
&'static
是一个对T
的不可变引用,可以在任意长的范围内被持有,甚至到程序终止。这当然要求T
本身是不可变的(创建共享引用后,原则上不再被修改),并且在创建引用后不会移动。T
不必在编译时创建,因为我们完全可以在运行时动态分配内存,返回一个'static
引用,代价是(可能的)内存泄漏,比如:
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
不仅包含 &'static T
,还包含所有权类型,比如String
,Vec
等等,范围更广。所有权类型保证数据的有效性,只要所有权类型被持有,就保证数据在之后任意长的范围内被持有,甚至到程序终止,自然符合'static
的意思。T: 'static
不应该被读作 “T
拥有 'static
生命周期”,应该是“T
被'static
生命周期绑定 ”。
所有权类型本身就是随时创建,允许被持有到程序结束,也可以随时drop。它可以被'static
绑定,就很好地反驳了 T: 'static
表示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 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 the program" ); }
关键点
T: 'static
应该被读作“T
被'static
生命周期绑定 ”。
T: 'static
表示T
是一个拥有 'static
生命周期的借用 或者 是一个所有权类型
原文这里用的就是with 'static
,人晕了,不应该是绑定吗?
由于T: 'static
中的T
包含所有权类型,也就意味着T
:
可以在运行时被动态创建
不必对整个程序有效
可以在safe Rust中自由地修改
可以在运行时被释放
可以有自由的生命周期
3) &'a
和T:'a
含义相同 这个误解是2)的泛化版本
&'a T
的出现,实际上也暗示着 T: 'a
。因为如果T
本身不能在范围 '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 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: 'static
那么 T: 'a
也成立,因为对任意 'a
都有 'static
>= 'a
('static
是所有生命周期类型的子类型)
4) 我的代码没有泛型,而且不需要生命周期 该误解的推论:
这个误解的出现,可能有Rust的生命周期省略规则(lifetime elision rules)
的责任。这些规则允许你在函数签名中省略生命周期标注,因为编译器会按照如下的规则进行推断:
每个输入引用都有不同的生命周期
如果只有一个输入引用,则所有输出引用都应用该生命周期
如果&self
、&mut 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 object
闭包
实际上都会应用被省略的生命周期标记
关键点:
几乎所有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()); }
我们实现了一个bytes切片的迭代器,并且成功调用next,看上去一切正常。但是当我们同时持有多个元素,会发生什么呢?
fn main () { let mut bytes = ByteIter { remainder: b"1123" }; let byte_1 = bytes.next(); let byte_2 = bytes.next(); if byte_1 == byte_2 { } }
报错了
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
要修复这个错误,我们猜测可以使用Copy,每次迭代都复制u8。这当然可以,但是如果我们扩展ByteIter
,变为一个泛型的迭代器,工作在&[T]
序列上,数据T
的复制可能是一个复杂甚至不可实现的操作,那怎么办呢?好吧,那好像没有什么可以做的了,毕竟代码已经正确编译了,生命周期也没有改进的余地了,对吧?
并不是,其实当前的生命周期标注正是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) } } }
好像没什么帮助,仍然让人困惑。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) } } }
这样我们能理解了,每个返回的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借用检查器对生命周期标记的使用,仅停留在静态验证内存安全性,语义层面不关心,即使有错误,只要内存安全就可以编译。比如上面例子中,语义上迭代器需要支持多个迭代引用同时存活,但是只支持一个存活也是内存安全的,所以编译通过,代价是程序变得过分严格,没必要。
修改后可以满足语义,借助的是编译器可以从结构体的字段中自动split出引用 —— rustonomicon。
这里还有一个反面的例子:生命周期的省略正好在语义上正确,我们显示标注的生命周期反而产生了一个“过分严格”的方法。
#[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
进行一次调用,之后结构体就被永久可变借用,几乎陷入不可使用的状态。当然这个情况非常罕见,但是迷糊的初学者还是容易写出。修复方法就是不要加额外的标记,让省略规则处理它。
#[derive(Debug)] struct NumRef <'a >(&'a i32 );impl <'a > NumRef<'a > { fn some_method (&mut self ) {} }fn main () { let mut num_ref = NumRef(&5 ); num_ref.some_method(); num_ref.some_method(); println! ("{:?}" , num_ref); }
因为使用了省略规则,some_method使用全新的生命周期参数,调用前新建一个作用域,泛型’b单态化到这个新建的作用域,调用结束后退出作用域,归还可变借用。
关键点:
Rust关于函数的生命周期省略规则并不是对任何情况都适用
在你程序的语义层面,Rust了解得有限,并不及你
Rust专家建议:给生命周期标记取描述性的名字
在显式放置生命周期标记时,多思考为什么
6) Box
托管的trait object
没有生命周期
额外参考
Trait object types - The Rust Reference (rust-lang.org)
Lifetime elision - The Rust Reference (rust-lang.org)
std::raw::TraitObject 本身是两个裸指针,其中有指向数据的引用,该引用的有效范围自然也就确定着 trait object 的生命周期。其他包含引用的结构体都会显式声明生命周期,比如ByteIter<'a>
,但是dyn SomeTrait
不方便采用类似的声明格式,于是加了另一种语法dyn SomeTrait + 'a
来表示 struct TraitObject
所绑定的生命周期。而大部分情况下,这种语法的生命周期都是自动推断的,这也是本节讨论的内容。
早前我们讨论了对函数 的生命周期省略规则。Rust 对 trait object 也有相应的生命周期省略规则:
如果 trait object 被用做一个外围类型的泛型参数,首先考虑基于该外围类型进行生命周期推断
如果外围类型有唯一的生命周期绑定,沿用它 (对应&'a dyn Trait
和 Ref<'a, dyn Trait>
)
如果外围类型有多个生命周期绑定,则需要显式指明 trait object 的生命周期 (对应 TwoBounds<'a, 'b, dyn Foo>
)
如果上述规则不适用,考虑下列规则:
如果 trait 本身定义包含单个生命周期绑定,默认使用 (对应 dyn GenericTrait<'a>
)
如果可以全部 'static
, 就用'static
. (对应 Box<dyn Trait>
?)
如果不存在生命周期绑定, 则利用表达式推断,非表达式就使用'static
(对应 impl dyn Trait
)
这些规则听起来超级复杂,但是可以简单地总结为:一个 trait object 的生命周期绑定可由从上下文推断 。通过一些例子,我们可以看到这种推断非常符合直觉,所以不必记忆上述规则。
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 42 43 44 45 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 >;struct TwoBounds <'a , 'b , T: ?Sized + 'a + 'b > { f1: &'a i32 , f2: &'b i32 , f3: T, }type T7 <'a , 'b > = TwoBounds<'a , 'b , dyn Foo>;trait GenericTrait <'a >: 'a {}type T8 <'a > = Box <dyn GenericTrait<'a >>; type <'a> = Box<dyn GenericTrait<'a> + 'a>;impl <'a > dyn GenericTrait<'a > {}impl <'a > dyn GenericTrait<'a > + 'a {}
实现 traits 的具体类型能够持有引用,因此类型本身拥有生命周期绑定,自然它的 trait object 也有对应的生命周期绑定。(即使实现 trait 的类型没有引用),还可以专门为引用类型实现 trait,这样 trait object 拥有生命周期绑定就显而易见了。
这里显然在解释为什么Box托管的trait object也有生命周期绑定,为什么不放在开头呢?然后再解释如何推断这些生命周期不是更自然,更容易理解吗?后面一段又说了spawn的例子,又是建立在默认推断 ’static
的知识上。啊这……这一段的行文我觉得有问题。
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> {}
(虽然这些规则很繁复、多数时候省略都OK,但是),无论怎样,审视这些规则是值得的,因为在某些场合,新手会被这个问题导致的奇怪错误搞迷糊,比如当他们把函数中的 trait object 重构为 泛型,后者反过来,将 泛型 重构为 trait object,比如下面这个例子
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
绑定,而第一个不需要?这就是因为 trait object 的生命周期默认推断,编译器实际上看到的第一个函数有’static
绑定。
关键点:
所有的 trait objects 都有一些默认推断的生命周期绑定
7) 编译器错误信息会指明如何修改我的程序 该误解的推论
Rust针对 trait objects 的生命周期省略规则总是正确的
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) | ^^^^^^^^^^^
这个推荐的修复信息,依据的是Box为 trait object 自动推断出的'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 objects 的生命周期省略规则并不是对任何情况都适用
在你程序的语义层面,Rust了解得有限,并不及你
Rust在错误信息中给出的修改意见虽然可以让程序通过编译,但可能并不是最适合你程序的方案。
8) 生命周期可以在运行时扩张或者缩小 该误解的推论:
容器类型可以在运行时交换(swap)引用,从而改变交换双方的生命周期
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 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" ); }
抛出错误
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
这个错误是因为Rust发现了替换动作,并选择了最短的生命周期进行绑定,然后静态分析到最后一句println
时,发现引用应该在更大的范围内有效,于是报错。如果删除最后一句话,这段代码可以编译:确定的周期是short,long被缩短后使用。
(尝试使用false,让替换分支在运行时不执行,看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" ); 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" ); }
由此我们知道,生命周期参数是在编译期就被静态验证的,并且借用检查器的控制流分析很初级,它假定每个if-else
的 block 都可能会执行,每个 match 的 arm 都可能会被选中,从而为(未限制执行生命周期的)变量选择一个最短的生命周期绑定。一旦生命周期被绑定,就永远被绑定了。变量的生命周期只有可能缩短(子类型当作父类型使用),但这种缩短,也是在编译期就被确定的。
关键点:
生命周期在编译时被静态验证
在运行时,变量的生命周期不会以任何形式发生改变
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); 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
这段代码中,(第三行) 我们的确执行了可变借用,但是立马重借出不可变借用(b),(期望)可变借用自动销毁。但Rust对待重借出的不可变引用(b)时,感觉和原有的可变借用如出一辙(b,c不能同时存活,变相等于可变引用并未销毁)。尽管上例中(即使放开限制,drop掉可变引用)不会出现问题,但是允许将可变引用直接降级为不可变引用(并且销毁(归还)原有的可变引用),确实会造成潜在的内存不安全:
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); }
这个例子的重点是,当你向可变引用 mut self 重借出共享引用时,你会陷入一个反直觉的陷阱:这个动作实际拓展了 mut self 的生命周期,和被借出的共享应用一样长(编译时确定作用域和生命周期参数),即使 可变借用 本身已经被drop
我一直不是很理解这里的”即使被drop“,既然编译器都已经给可变引用mut self
和重借出引用&str
附上了相同的生命周期参数,那显然mut self
就没有释放啊,作者的思维还是停留在上一个例子里?)
使用重借出引用很麻烦(反直觉),它本身虽然是不可变的,但是却不能和其他不可变引用同时存活。重借出引用有 可变引用 和 不可变引用 的缺点,却没有他们的优点。我认为“向可变引用重借出不可变引用”这一行为,在Rust中属于反模式的行为。对这种反模式保持警惕很重要,当你看到下面这样的代码时,可以轻松地识别到它。
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); }
出错原因是entry
语法中or_default
返回的是&mut Player
,由于显式的类型标注,发生了隐式的重借出。完成相同的目的,我们需要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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);
这有点笨拙和繁复,但也算我们为”内存安全祭坛“献上的祭品吧。
关键点:
尽量不要向可变引用重借出共享引用,否则你会很难受
对一个可变引用重借出,并不会终止它的生命周期(会伴随借出的共享引用,随时备查),即使它被drop
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 }; }
并没有一个好的理由来解释这种差异的出现。最早闭包实现的时候就用了不同的类型推断语义,现在统一也来不及了,因为这会是一个不向后兼容的修改。所以我们如何显式标注一个闭包的类型?可能的方法有:
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); }
关键点:
11) 'static
引用总能强制转换为'a
引用 之前我已经举过这个例子:
fn get_str <'a >() -> &'a str ; fn get_str () -> &'static str ;
一些读者曾联系我,询问这两种方法是否有现实意义的区别。经过探究,答案是肯定的,他们确实有区别。
通常对值来说,在使用'a
绑定引用的地方,我们总可以使用'static
绑定引用进行替换,因为Rust会把'static
引用强制转换为'a
引用。(比如代码期待&'a i32
的地方,我们总可以使用&'static i32
)。这是符合直觉的,在期待短生命周期引用的地方使用长生命周期引用,并不会产生内存安全问题(长生命周期的引用必然在更小的作用域内保持有效)。下面的代码可以如期编译:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 use rand;fn generic_str_fn <'a >() -> &'a str { "str" }fn static_str_fn () -> &'static str { "str" }fn a_or_b <T>(a: T, b: T) -> T { if rand::random() { a } else { b } }fn main () { let some_string = "string" .to_owned(); let some_str = &some_string[..]; let str_ref = a_or_b(some_str, generic_str_fn()); let str_ref = a_or_b(some_str, static_str_fn()); }
然而,当引用属于函数类型签名的一部分时,这种转换不会生效
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 rand;fn generic_str_fn <'a >() -> &'a str { "str" }fn static_str_fn () -> &'static str { "str" }fn a_or_b_fn <T, F>(a: T, b_fn: F) -> T where F: Fn () -> T { if rand::random() { a } else { b_fn() } }fn main () { let some_string = "string" .to_owned(); let some_str = &some_string[..]; let str_ref = a_or_b_fn(some_str, generic_str_fn); let str_ref = a_or_b_fn(some_str, static_str_fn); }
抛出这样的错误
error[E0597]: `some_string` does not live long enough --> src/main.rs:23 :21 |23 | let some_str = &some_string[..]; | ^^^^^^^^^^^ borrowed value does not live long enough ...25 | let str_ref = a_or_b_fn(some_str, static_str_fn); | ---------------------------------- argument requires that `some_string` is borrowed for `'static `26 | } | - `some_string` dropped here while still borrowed
这是否属于 Rust 缺陷还存在争议,毕竟前一个例子中,是在值上做直接转换,从 &'static str
到 &'a str
。但是当前例子是在转换类型,从 for<T> Fn() -> &'static T
到 for<'a, T> Fn() -> &'a T
。
关键点:
拥有签名for<'a, T> fn() -> &'a T
的函数,比for<T> fn() -> &'static T
更加灵活,适应更多的场景
结论
T
是&T
和&mut
的超集
&T
和&mut T
不相交
T: 'static
应该被读作“T
被'static
生命周期绑定 ”。
T: 'static
表示T
是一个拥有 'static
生命周期的借用 或者 是一个所有权类型 (原文这里用的就是with 'static
,人晕了,不应该是绑定吗? )
由于T: 'static
中的T
包含所有权类型,也就意味着T
:
可以在运行时被动态创建
不必对整个程序有效
可以在safe Rust中自由地修改
可以在运行时被释放
可以有自由的生命周期
T:'a
比 &'a T
更广泛,更灵活
如果 T: 'static
那么 T: 'a
也成立,因为对任意 'a
都有 'static
>= 'a
('static
是所有生命周期类型的子类型)
几乎所有Rust代码都是泛型代码,同时到处都有被省略的生命周期标记。
Rust关于函数的生命周期省略规则并不是对任何情况都适用
在你程序的语义层面,Rust了解得有限,并不及你
Rust专家建议:给生命周期标记取描述性的名字
在显式放置生命周期标记时,多思考为什么
Rust在错误信息中给出的修改意见虽然可以让程序通过编译,但可能并不是最适合你程序的方案。
生命周期在编译时被静态验证
在运行时,变量的生命周期不会以任何形式发生改变
Rust的借用检查器假定所有分支都会被命中,为变量选择最短的生命周期。
尽量不要向可变引用重借出共享引用,否则你会很难受
对一个可变引用重借出,并不会终止它的生命周期(会伴随借出的共享引用,随时备查),即使它被drop
每门语言都有陷阱🤷
拥有签名for<'a, T> fn() -> &'a T
的函数,比for<T> fn() -> &'static T
更加灵活,适应更多的场景
讨论 在这些地方讨论这篇文章
获取通知
拓展阅读