Rust-生命周期与引用有效性
Rust中的每个引用都是有其 生命周期 (lifetimes),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以Rust需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
据了解,在 Rust 官方的一次调查中,当受访者被问及对于提升 Rust 的采用率有何建议时,许多人提到的一个方案是降低 Rust 的学习难度。再具体到特定主题的难度时,许多人认为 Rust 的“生命周期(Lifetimes)”难度最高,其次是 Ownership,61.4% 的受访者表示,生命周期的使用既棘手又非常困难。而这两个功能又恰恰是 Rust 内存安全特性的核心。
生命周期的概念从某种程度上说不同于其它语言中类型的工具,毫无疑问这是Rust最与众不同的功能。
生命周期避免了悬垂引用
生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。如下示例,它有一个外部作用域和一个内部作用域:
{ let r; { let x = 5; r = &x; } println!("r:{}", r); }
外部作用域声明了一个没有初值的变量r,而内部作用域声明了一个初值为5的变量x。在内部作用域中,我们尝试将r的值设置为一个x的引用。接着在内部作用域结束后,尝试打印出r的值。这段代码不能编译因为r引用的值在尝试使用之前就离开了作用域。错误信息如下:
error[E0597]: `x` does not live long enough --> src/main.rs:245:13 | 245 | r = &x; | ^^ borrowed value does not live long enough 246 | } | - `x` dropped here while still borrowed 247 | println!("r:{}", r); | - borrow later used here
变量x并没有"存在的足够久"。其原因是x在到达第246行内部作用域结束时就离开了作用域。不过r在外部作用域仍是有效的;作用域越大我们就说它“存在的越久”。如果Rust允许这段代码工作,r将会引用在x离开作用域时被释放的内存,这时尝试对r做任何操作都不能正常工作。那么Rust是如何决定这段代码是不被允许的呢?这得益于借用检查器。
还有以下说明生命周期:
let a = 100; { let x = &a; }//x的作用域结束 println!("x:{:?}",x);
编译时,会出现一个错误:
error[E0425]: cannot find value `x` in this scope --> src/main.rs:174:24 | 174 | println!("x:{:?}",x); |
在Rust中也存在作用域概念,当资源离开作用域后,资源的内存就会被释放回收,当借用/引用离开作用域后也会被销毁,所以x在离开自已的作用域后,无法在作用域之外访问。
上面的涉及到几个概念:
- Owner: 资源的所有者 a
- Borrower: 资源的借用都x
- Scope: 作用域,资源被借用/引用的有效期
借用检查器
Rust编译器有一个借用检查器(borrow checker),它比较作用域用确保所有的借用都是用效的。
{ let r; // ---------+-- 'a // | { // | let x = 5; // -+-- 'b | r = &x; // | | } // -+ | // | println!("r: {}", r); // | } // ---------+
r和x的生命周期注解,分别叫做 'a 和 'b
这里将r的生命周期标记为'a并将x的生命周期标记为'b。如上面的展示,内部的'b块要比外部的生命周期'a小得多。在编译时,Rust比较这两个生命周期的大小,并发现r拥有生命周期'a,不过它引用了一个拥有生命周期'b的对象。程序被拒绝编译,因为生命周期'b比生命周期'a要小:被引用的对象比它的引用者存在的时间更短。
让我们看看以下并没有产生悬垂引用且可以正确编译的例子:
{ let x = 5; let r = &x; println!("r:{}", r); }
这里x拥有生命周期'b,比'a要大。这就意味着r可以引用x:
Rust知道r中的引用在x有效的时候也总是有效的。
Lifetime 推导
要推导Lifetime是否合法,先明确两点:
- 输出值(也称为返回值)依赖哪些输入值
- 输入值的Lifetime大于或等于输出值的Lifetime (准确来说:子集,而不是大于或等于)
Lifetime推导公式: 当输出值R依赖输入值X Y Z ...,当且仅当输出值的Lifetime为所有输入值的Lifetime交集的子集时,生命周期合法。
Lifetime(R) ⊆ ( Lifetime(X) ∩ Lifetime(Y) ∩ Lifetime(Z) ∩ Lifetime(...) )
对于例子1:
//'a是Lifetime的标识符 //因为编译器无法推导出返回值的Lifetime应该是比x长,还是比y长。 //所以要显式指出Lifetime标识符 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
因为返回值同时依赖输入参数x
和y
,所以
Lifetime(返回值) ⊆ ( Lifetime(x) ∩ Lifetime(y) )
即:
'a ⊆ ('a ∩ 'a)//成立
定义多个Lifetime标识符
那我们继续看个更复杂的例子,定义多个Lifetime标识符:
fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { if x.len() > y.len() { x } else { y } }
先看下编译,又报错了:
error[E0623]: lifetime mismatch --> src/main.rs:214:9 | 210 | fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { | ------- ------- | | | this parameter and the return type are declared with different lifetimes... ... 214 | y | ^ ...but data from `y` is returned here
编译器无法正确地推导返回值的Lifetime,我们可能会疑问,“我们不是已经指定返回值的Lifetime为'a了吗?”
这里我们同样可以通过生命周期推导公式推导:
因为返回值同时依赖x和y,所以:
Lifetime(返回值) ⊆ ( Lifetime(x) ∩ Lifetime(y) )
即:
'a ⊆ ('a ∩ 'b)//不成立
很显然,上面我们根本没法保证成立。
所以,这种情况下,我们可以显式地告诉编译器'b
比'a
长('a
是'b
的子集),只需要在定义Lifetime的时候, 在'b
的后面加上:'a
, 意思是'b
比'a
长,'a
是'b
的子集:
fn foo<'a, 'b:'a>(x: &'a str, y: &'b str) -> &'a str { if x.len() > y.len() { x } else { y } }
这里我们根据公式继续推导:
条件:Lifetime(x) ⊆ Lifetime(y)
推导:Lifetime(返回值) ⊆ ( Lifetime(x) ∩ Lifetime(y) )
即:
条件: 'a ⊆ 'b
推导:'a ⊆ ('a ∩ 'b) // 成立
上面是成立的,所以可以编译通过。
推导总结
我们要记住两点:
- 输出值依赖哪些输入值。
- 推导公式。
Lifetime Bound
就像其它泛化类型参数类型,可以使用lifetime bound来约束一个类型T或另一个lifetime 'b,以要求类型T或'b满足一定的条件;
使用 : 来表示bound约束,类似与类型参数/Trait的bound约束,lifetime bound有如下形式及语义:
'a: 'b 表示lifetime 'a必须outlive lifetime 'b即'a的范围至少与'b的范围一样大;
T: 'a 表示类型T中包含的所有引用对应的lifetime必须outlive lifetime 'a即包含的引用在lifetime 'a内可被有效访问;
T: Trait + 'a 表示类型T必须实现trait Trait同时T包含的所有引用在lifetime 'a 内可被有效访问;
struct Ref<'a, T: 'a>(&'a T) ;
上面Ref定义表示:结构体Ref包含一个指向泛化类型T对象实例的引用,该引用具有lifetime 'a,对应的T对象实例具有lifetime 'a,类型T内所有的引用必须outlive lifetime 'a;
另外Ref结构体的对象实例本身不能outlive lifetime 'a;
另外出现'a: 'a从语法上是合理的,表示lifetime 'a的范围至少与自身的范围一样大,但'a上bound后,则它变成early bound;
lifetime Elision
虽然每一个函数中涉及到的引用参数都可以显式的使用lifetime标记anotations,但在有些场景下可以省略annotations,由编译器自动推导生成一个annotation,以便减少代码的编写和减轻开发者的负担;
触发lifetime Elision的规则如下:
- 函数的每一个引用参数都要有自身对应的lifetime;
- 如果函数正好只有一个引用输入参数,则所有引用输出参数都使用输入参数lifetime;
- 如果函数有多个输入参数,其中有一个为&self或&mut self,则所有引用输出参数都使用&self或&mut self的lifetime;
fn first_word(s: &str) -> &str {} //等价 fn first_word<'a>(s: &'a str) -> &'a str {}
由于没法满足B,C规则,上面提到的longest函数中必须显示写出lifetime标记,否则会编译出错,
因为不显示标记,则无法确定两个输入参数和一个输出参数之间的lifetime约束;
如下示例不显示标记,不能通过编译:
fn longest(x: &str, y: & str) -> & str { if x.len() > y.len() { x } else { y } }
error[E0106]: missing lifetime specifier --> src/main.rs:160:34 | 160 | fn longest(x: &str, y: & 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 `x` or `y` help: consider introducing a named lifetime parameter | 160 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { | ^^^^ ^^^^^^^ ^^^^^^^^ ^^^
加上lifetime标记后通过编译:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
特殊标记'static
静态生命周期'static:是Rust内置的一种特殊的生命周期。'static生命周期存活于整个程序运行期间。所有的字符串字面量都有生命周期,类型为 & 'static str。
标记 'static 作为Rust语言保留的lifetime标记标识,可用来表示引用的lifetime,可以用作lifetime bound来约束其它类型;
/* A reference with 'static lifetime: 表示被引用的对象在程序整个生命周期都有效; 引用本身可在程序退出前的整个生命周期都可安全有效使用; 具有'static的引用可以转换成更小的子范围/lifetime 'b引用并被使用; */ let s: &'static str = "hello world";
/* 'static as part of a trait bound: 'static用作bound,表示类型T没有包含非'static引用, 作为类型T的对象实例接受都可在不同上下文中使用该实例,直到析构该实例; 而不是表示类型T的对象实例的生命周期是'static; */ fn generic<T>() where T: 'static {}
lifetime标记用作结构体泛化参数
在struct中Lifetime同样重要,我们来定义一个Person结构体:
struct Person { age: &u8, }
编译时我们会得到一个error:
--> src/main.rs:219:10 | 219 | age: &i32, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 218 ~ struct Person<'a> { 219 ~ age: &'a i32,
之所以会报错,因为Rust要确保Person的Lifetime不会比它的age借用长,不然会出现Dangling Pointer的严重内存问题。所以我们需要为age借用声明Lifetime:
struct Person<'a> { age: &'a u8, }
不需要对Person后面的<'a>感到疑惑,这里的'a并不是指Person这个struct的Lifetime,仅仅是一个泛型参数而已,struct可以有多个Lifetime参数用来约束不同的field,实际的Lifetime应该是所有field Lifetime交集的子集。详细看这里。
泛化结构体可使用lifetime标记annotations比如'a、'b作为泛化参数,它作为一个可后续具体化的lifetime参数,其字段可以使用它作为引用类型变量定义的一部分;
struct ImportantExcerpt<'a>{ part: &'a str }
let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next() .expect("Could not find a '.'"); let i = ImportantExcerpt{ part: first_sentence, };
上面示例代码表达的语义是:
结构体ImportantExcerpt对象实例不能outlives它的子字段part对应的引用的lifetime 'a;
子字段part在lifetime 'a内有效,可接受来自于lifetime 'a的str对象的引用赋值,或者在lifetime 'a范围内安全读取或为lifetime 'a范围内的引用变量赋值;
这个lifetime 'a同时对结构体ImportantExcerpt类型的对象实例和其它字段part的lifetime进行lifetime 'a约束;
为什么这个'a需要对结构体ImportantExcerpt类型的对象实例和字段part都需要产生约束呢?
其实从语义上理解非常的直接,因为如果结构体的对象实现容许在lifetime 'a之外使用,那么通过这个对象访问其子字段理应同样被容许,但从定义上要求子字段的有效lifetime为'a,现在超出了'a代表的范围访问,显然会导致悬空引用,所以逻辑上不容许,于是对结构体ImportantExcerpt的对象实例也要产生约束;
在实例ImportantExcerpt对象前需要early bound提前确定lifetime 'a的值,其具体值由创建时传递的参数first_sentence来决定;
trait及impl中使用lifetime标记
由于lifetime标记'a是以泛化参数的形式出现在结构体ImportantExcept定义中,是结构体泛化参数的一部分;
类似与泛化中的类型参数,在impl method或trait时,同样需要以泛化参数的形式比如impl<'a>满足语法上的要求;
为结构体提供level方法实现的示例代码如下:
impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
2020-09-24 739. Daily Temperatures
2020-09-24 17. Letter Combinations of a Phone Number