导读
本文是笔者对Common Rust lifetime misconceptions一文的理解。
作者使用Rust 4多年,整理了这份针对lifetime的常见误解以及纠正。误解如何产生呢?笔者想大概要归因于早期的文章和资料不够充分全面,许多人(比如笔者)会用其他编程语言的经验往rust上套,导致认识与Rust实际机制的偏差。包括笔者写的这篇文章,也可能让读者产生新的误解。
不管是萌新还是老手,原文都值得一读。说不定将来招Rust开发,面试官会拿里面的内容来提问呢 :-)
内容
泛型T
泛型参数T
代表了所有类型,包括所有非引用类型,不可变引用、可变引用类型。因此:
impl<T> Trait for T{}
impl<T> Trait for &T{} // 编译失败: 重复实现
T:’static
这一段重点讨论 static variable
, T:'static
, &'static T
三者的差异。
首先是静态变量,它的特点是:
- 编译期分配空间,运行时不再修改。
- 可变或者不可变。修改静态变量只能在unsafe代码中进行。
- 整个程序运行期间都存活。
&'static T
说明对类型为T的对象obj的借用范围为 'static
,但这并不代表obj必须是静态变量。 Box::leak()
方法可以以内存泄漏为代价, 在运行时创建在程序运行期间一直存活的对象。
T:'static
表示 :
T is bounded by a ‘static lifetime.
这个表示很有意思,因为大多数人将 T:'static
理解为”T具有’static生命周期”。
所以它表示的含义为:
- T可以内部带有
'static
引用的类型, 如Vec<&'static i32>
- 也可以不带引用的类型, 例如:
i32
,Vec<i32>
,Vec<String>
一个奇怪的地方:
fn miao<T:'static + Debug>(d: T){
println!("{:?}", d);
}
fn main(){
miao(vec![1, 2, 3]); // ok
miao(vec![&1, &2]); // 也ok,可能与编译器优化有关。
let d = 10;
miao(vec![&d]); // error, d不能产生'static借用
}
&’a T与T:’a
这一节是对上一节 T:'static
问题的泛化。'static
是最大的生命周期, 而’a表示所有可能的生命周期,它在编译时具体化。
从父类子类的角度看,范围越大的生命周期约束是范围小的约束的子类。这就好比:
- 我一天工作24小时, 那么10:00-18:00这段时间我在工作
- 10:00-18:00这段时间我在工作, 但我不能说我一天工作24小时
T:'a
具体表示的含义为:
- T可以带有引用,其引用的生命周期类型为
'a
的子类,也就是生命周期范围要不小于'a
的范围。如Vec<&'static i32>
,Vec<&'a i32>
。 - 也可以不带’a引用的类型, 例如:
i32
,Vec<i32>
,Vec<String>
通过编译不等于语义正确
不一定,rust编译器对注解的要求是内存安全即可。同时rust编译器会隐式标记,可能会导致与开发者想要的语义不一样。
另一方面,开发者错误标记注解,可能会导致过分严格的生命周期限制:
struct S<'a>{
data: &'a i32,
}
impl<'a> S<'a>{
// 具体语义为:调用此方法产生的借用将持续到S被回收
// 导致的结果是一旦这个方法被调用, 接受者将无法再次产生其他借用
fn method(&'a mut self){}
}
Trait Object也有lifetime constraint
这是因为大多数情况下,编译器自动推导了生命周期约束。
type BoxA = Box<dyn Trait>;
type BoxA = Box<dyn Trait + 'static>; // 如果trait不含生命周期参数,将推导为'static
trait T<'a>{}
type BoxB<'a> = Box<dyn T<'a>>;
type BoxB<'a> = Box<dyn T<'a> + 'a>;
编译器的提示不一定符合开发者想要的语义
编译失败,编译器提出的解决办法可能过于保守:
// 编译失败
fn box<T:Display>(t:T) -> Box<dyn Display>{
Box::new(t)
}
// 编译器推荐这样
fn box<T:Display + 'static>(t:T) -> Box<dyn Display>{
Box::new(t)
}
// 实际上开发者想要的可能是这样
fn box<'a, T:Display + 'a>(t:T) -> Box<dyn Display + 'a>{
Box::new(t)
}
lifetime constraint是编译期属性
不能在运行时动态改变对象的lifetime constraint。如果对象内部有多个借用,那么会挑选其中最小的范围作为约束(如果有借用与其他借用没有交集,编译错误),一旦确定了这个lifetime就不会更改。
&mut T转为&T不一定安全
先介绍Rust的re-borrowing:
允许对借用一个引用, 也即是
let b = & &a
。在reborrowing下,允许同一时间存在多个对原对象的可变借用,但是它们对原对象的访问不能重叠。fn main(){
let mut a = 1;
let mut b = &mut a;
let c = &mut b; // re-borrowing, ok
// compile error
//let d = &mut a;
// 如果交换下面两行就会编译错误
println!("{}", c); // compiler到这里可以回收c
println!("{}", b);
}
这一节主要讨论一个误解:re-borrowing一个引用会立刻回收它,产生另一个引用。但实际上:
let mut a = 10;
let b = &*(&mut a); // 看起来&mut a会被立刻回收,但实际上它持续到最后一次使用b, 也就是第5行
let c = &a; // 编译失败: 借用冲突了, 可改为 let c = &b;
println!("{}", c);
println!("{}", b);
闭包中的生命周期与函数不太一样
目前的Rust中,生命周期机制在闭包和函数中有差异。这部分建议阅读原文。
&’static str不一定会转为&’a str
作者给了个例子,这个例子也与闭包相关:
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); // compiles
let str_ref = a_or_b_fn(some_str, static_str_fn); // compile error
}
直觉告诉我们,编译器要进行生命周期约束的转换: &'static str
可以转为 &'a str
;但实际上,编译器报错了,因为它打算对str_ref
产生 &'static str
借用,这是不行的。