引言 这是Rust九九八十一难第十六篇。上篇聊了下Rust有哪些危险操作,有个操作是无脑Clone的风险,这时候可能用到引用和’a生命周期注解(Lifetime annotations)。那么问题来了,有时候必须加,有时不用,到底什么时候用’a,生命周期的计算规则是什么等等。这篇就整理下生命周期注解的使用。
一、简单了解和入门 使用之前,先做个入门。Lifetime annotations(’a,’b等)像是一种约束,只是告诉编译器引用之间的有效期关系,编译器以此和borrow-checker,保证引用 不会比引用数据活 的更久。有点类似学校的某班点名册,学生是数据,点名册是引用,他俩关系是学生毕业了,这本点名册就要废弃。
1、函数返回引用 下面的函数接受俩字符串切片,返回返回其中较长的一个
错误使用:
1 2 3 4 5 6 7 fn longest (x: &str , y: &str ) -> &str { if x.len () > y.len () { x } else { y } }
编译报错:Missing lifetime specifier [E0106]
正确使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn longest <'a >(x: &'a str , y: &'a str ) -> &'a str { if x.len () > y.len () { x } else { y } } fn main () { let s1 = String ::from ("hello world" ); let s2 = String ::from ("hi" ); let result = longest (&s1, &s2); println! ("Longest: {}" , result); }
这里&'a str表示&str在用在某个生命周期 'a 内有效,x 和 y 的引用都有生命周期 'a,函数返回一个同样具有 'a 生命周期的引用。编译器知道后,保证x和y活多久,只要它们都活过同一段 'a,那么返回的引用就是安全的。
2、struct\enum使用生命周期注解 用结构体存引用,这必须给struct加生命周期注解。比如下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #[derive(Debug)] struct Book <'a > { title: &'a str , author: &'a str , } fn main () { let title = String ::from ("Rust in Action" ); let author = String ::from ("Tim McNamara" ); let book = Book { title: &title, author: &author, }; println! ("{:?}" , book); }
Book 存有引用字段,Rust需要知道引用在哪段时间有效(防止悬垂引用、推导借用规则等),于是 Book<'a> 中 'a 表示:这个 struct 所持的引用至少活到 'a,且 Book<'a> 的实例本身不能比 'a 活得更久。也就是说:
'a 是一个 约束条件 :Book 内部引用必须有效
struct 的生命周期 ≤ 'a
二、Lifetime annotations命名规则 1、规则和限制
必须以 ' 开头,所有生命周期名称都必须以单引号 ' 开头,比如:’a,’static。
'a、'b、'c 是最常见的
生命周期名可以是多个单词或多个字符(但必须合法的标识符):
1 fn parse <'de >(s: &'de str ) { }
必须是合法标识符字符(字母、数字、下划线),但不能以数字开头:
合法: ‘file、’input1 、’_a
不合法:’1abc、’’abc // 双引号
'static(内置生命周期),可以使用 'static,它不是关键字,但它的意义被语言定义。
'_'(匿名生命周期),从 Rust 1.34 开始允许,用于减少显式写法
1 fn foo<'_>(x: &'_ str) {}
Rust 没有任何关键字被禁止用作生命周期名 。但随便用关键字会降低可读性。
2、一些建议
如果不需要写生命周期,就别写,写了会传染
简单场景用 'a 足够,不要过度设计
多个生命周期时,用 'a 'b 'c,除非语义重要
不关心具体生命周期用’_
程序整个生命周期,’static
生命周期与类型参数顺序:先 'a 后 T
1 fn f <'a , T>(x: &'a T) {}
特定场景
1 2 3 fn parse<'de, 'input>(src: &'input str) -> Result<T<'de>, E> { ... }
上下文:’ctx
配置:’cfg
字符串来源:’s
默认使用 'a', 'b', 'c'。只有当它们导致混淆,或者你需要为特定的外部数据源提供明确的上下文时,才考虑使用更长的、描述性的名称(如 'repo, 'src')。
三、生命周期注解如何计算存活时间 Rust 生命周期注解本身并不计算或控制引用的存活时间,它是描述性 的。它的作用是告诉 Borrow Checker不同引用之间的生命周期关系,从而让编译器能够检查引用是否有效,防止悬垂引用的产生。
Rust lifetime = 取“引用被使用的区间”与“被借用对象的存活区间”的交集,并确保借用不超过对象范围,如果涉及多个借用,则返回引用的生存区间必须是输入引用交集中的某个合法子集。通过几个例子说明下:
四、使用场景:哪些情况必须使用vs哪些不用 既然生命周期注解有这么重要,为什么好多情况不加也没问题呢,到底什么情况下需要?这就涉及到生命周期省略规则。
1、Rust在这些场景下能自动推断生命周期,无需手动加标注。 a、所有输入引用各自都有一个推断到的生命周期 函数参数里每个 &T,如果没写生命周期,编译器会自动给它加一个,但不返回引用 时或者返回值不是引用。
例如:fn foo(x: &str, y: &str) 等价于 fn foo<'a, 'b>(x: &'a str, y: &'b str)。
b、只有一个输入引用,则输出引用用它的生命周期 如果函数 有且只有一个输入引用 ,并且返回引用, 那么返回值的生命周期会自动和这个输入一样。这是最安全的默认推断:如果返回一个引用,那么它必须依赖于唯一的输入引用
例如:fn foo(x: &str) -> &str 等价于 fn foo<'a>(x: &'a str) -> &'a str。
c、方法中有 &self / &mut self,输出引用默认跟 self 的生命周期 假设方法是借用(而非所有权转移),返回的任何引用最有可能指向方法调用者(self)所拥有的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct Owner { data: String, } impl Owner { // 原始代码:没有手动标注 fn get_data_ref(&self) -> &str { &self.data // 返回对自身数据的引用 } } // 编译器推断:等价于 /* impl<'a> Owner { fn get_data_ref(&'a self) -> &'a str { &self.data } } */ // 约束:返回的 &str 的生命周期 'a 与 &self 的生命周期 'a 相同。
d、结构体没有引用字段
2、必须加生命周期标注 a、结构体(或枚举)里含有引用字段 1 2 3 struct Foo <'a > { name: &'a str , }
结构体持有引用必须明确生命周期,否则编译器不知道引用活多久。
b、函数/方法返回的引用来自多个输入引用(多对一),编译器无法判断返回的是谁 1 fn choose (x: &str , y: &str ) -> &str { ... }
这会报错,因为编译器不知道返回值应该跟 'x 还是 'y。必须写:
1 fn choose <'a >(x: &'a str , y: &'a str ) -> &'a str
c、方法返回的引用不属于 self,但有多个输入引用 1 2 3 fn pick <'a >(&self , other: &'a str ) -> &str { other }
如果不写:
1 fn pick (&self , other: &str ) -> &str
编译器会以为输出是 self 的生命周期(规则 3),导致错误。
3、特殊场景 3.1、 声明生命周期的“长短关系” 'a: 'b 你有两个输入生命周期 'a 和 'b,但你要保证 'a >= 'b('a 活得至少跟 'b 一样久),否则返回 'b 的引用可能会出错。
1 2 3 4 5 6 fn choose_first <'a , 'b >(x: &'a str , _y: &'b str ) -> &'b str where 'a : 'b , { x }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct Mapper <'a , 'b > { from: &'a str , to: &'b str , } impl <'a , 'b > Mapper<'a , 'b >where 'a : 'b , { fn map (&self ) -> &'b str { self .from } }
'a: 'b 是 Rust 生命周期 subtyping(子类型)约束的语法,用来表达:
生命周期 'a 必须比 'b 活得更久(或至少一样久) 换句话说:'a ≥ 'b
3.2、trait object 中的生命周期(如 Box<dyn Trait + 'a>) 这个 Box 必须至少活 'a 那么久,否则其中的引用就可能悬垂。通常情况下不需要,但在某些特定场景下是必须的。在绝大多数情况下,当写 Box<dyn Printer> 时,编译器会自动添加生命周期参数。
Box<dyn Trait>等同于 'static,不能放非 'static 引用。只有当 Trait Object 内部的类型依赖于一个非 'static' 的,即局部作用域 的生命周期时,才必须手动添加 'a 来约束它。
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 trait Printer { fn print (&self ); } struct StrPrinter <'a > { msg: &'a str , } impl <'a > Printer for StrPrinter <'a > { fn print (&self ) { println! ("{}" , self .msg); } } fn make_printer <'a >(msg: &'a str ) -> Box <dyn Printer + 'a > { Box ::new (StrPrinter { msg }) } fn main () { let s = String ::from ("hello" ); let p = make_printer (&s); p.print (); }
上面例子创建一个 StrPrinter,并且这个对象内部封装了一个生命周期短于 'static' 的引用 。省略 'a' 会导致编译器默认采用 '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 28 29 30 31 32 33 34 35 36 37 38 39 trait Logger { fn log (&self , message: &str ); } struct OwnedLogger { prefix: String , } impl Logger for OwnedLogger { fn log (&self , message: &str ) { println! ("[{}] {}" , self .prefix, message); } } fn create_logger (prefix: &str ) -> Box <dyn Logger> { Box ::new (OwnedLogger { prefix: prefix.to_string (), }) } fn main () { let msg = String ::from ("Main scope message" ); let logger_box = create_logger ("APP" ); logger_box.log ("User logged in." ); let static_logger_box = create_logger ("STATIC" ); static_logger_box.log ("System started." ); }
3.3、HRTB 必须加 for<'a>
HRTB(Higher-Rank Trait Bounds,高阶生命周期约束)
HRTB 使用 for<'a> 语法,是因为它用于声明 或约束 一个类型(通常是闭包或函数指针)的能力,即该类型对于任意 传入的生命周期都是有效的。
1 2 //这里的 fn(&i32) 实际上隐含了某个生命周期 'x fn call_fn(f: fn(&i32)) { ... }
如果你想让 f 可以接受 任意生命周期 的引用,就需要 HRTB:
1 2 3 4 fn call_fn <F>(f: F)where F: for <'a > Fn (&'a i32 ), { ... }
这里 for<'a> 告诉 Rust:f 对 所有生命周期 'a 的引用 都有效
举个例子
1 2 3 4 5 6 7 8 9 10 11 fn call_with_i32 <F>(f: F)where F: for <'a > Fn (&'a i32 ), { let x = 5 ; let y = 10 ; f (&x); f (&y); } call_with_i32 (|n| println! ("{}" , n));
什么时候必须用HRTB(for<'a>)
3.4 、GATs需要加’a场景 GATs (Generic Associated Types),即通用关联类型 ,是 Rust 语言中一项重要的、相对高级的特性。它允许 Trait(特型)的关联类型 能够拥有自己的泛型参数 (包括生命周期参数和类型参数),就像 Trait 本身可以拥有泛型参数一样。使用 GATs,可以定义一个关联类型 Ref,并为其引入一个生命周期参数 'a,从而明确这个引用依赖于 'a。
语法:
1 2 3 4 5 6 7 8 trait MyTrait { // T 是 Trait 的泛型参数 // Item<U> 是关联类型 Item 的泛型参数 type Item<U>; // 允许关联类型依赖于生命周期参数 'a type Reference<'a> where Self: 'a; }
在迭代器场景,Iterator::Item 是一个 独立于迭代器(self)生命周期的类型。没有GATs,不能 安全地写出“返回对自身内部数据的引用”的迭代器。
举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 pub trait LendingIterator { // GATs: 定义一个关联类型 Item,它依赖于生命周期参数 'a // 这里的 Item<'a> 表示“一个生命周期为 'a 的引用” type Item<'a> where Self: 'a; // next 方法返回的 Item<'a>,其生命周期 'a 取决于 &self 的生命周期 fn next<'a>(&'a self) -> Option<Self::Item<'a>>; } struct DataProcessor { data: Vec<i32> } impl LendingIterator for DataProcessor { // 实例化 Item<'a> 为一个依赖于 'a 的引用 type Item<'a> = &'a i32; fn next<'a>(&'a self) -> Option<Self::Item<'a>> { // 实际代码会复杂的多,这里只是示意 self.data.get(0) } }
Item<'a> 不是一个固定类型 ,而是一个模板 ,根据 'a 不同得到不同的具体类型。Item<’short>会变成&’short str, Item<’long>会变成&’long str
next 返回的内容与 &self 生命周期保持一致,保证安全
五、总结 本文总结了生命周期注解的使用方式、命名规则和必须使用的场景等,优点是帮助编译器更好的管理。缺点也不少,比如学习复杂;注解有传播性,一旦一个类型(struct、enum、函数)带上生命周期 'a,那么它的使用者可能也必须带上 'a,项目大,可能会反省爆炸;编译器无法推断所有场景。使用的时候,能不加就不加,仅在编译器报错/性能需求/结构设计确实要求借用时,才加生命周期注解。
如果觉得本文有用,本人公众号大鱼七成饱,辛苦点个关注吧。