导读
本文是笔者对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]); // okmiao(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不含生命周期参数,将推导为'statictrait 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到这里可以回收cprintln!("{}", 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) -> Twhere 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); // compileslet str_ref = a_or_b_fn(some_str, static_str_fn); // compile error}
直觉告诉我们,编译器要进行生命周期约束的转换: &'static str 可以转为 &'a str ;但实际上,编译器报错了,因为它打算对str_ref 产生 &'static str 借用,这是不行的。
