开篇
01
本篇文章将介绍 Rust 的 lifetime。lifetime 这也是 Rust 中的重点。简单了解下 Rust 的生命期的概念以及简单使用。本篇文章的阅读时间大约 10 分钟。
题外话
02
文章开头,我建议大家不要将 lifetime
翻译成 生命周期
。其实life circle
这个词组的意思才是 生命周期
。来看下lifetime
的有道翻译。
通常我们所说的生命周期应该是存在一个循环过程的。另外,在 Rust 中,只有引用类型才需要标注 lifetime。lifetime 是用来保证引用类型在使用时是有效的,并且在使用结束后释放所占用的内存的一种机制。所以,我们可以将其翻译为引用的生存期,引用的有效期,引用的使用期等等。
lifetime (生命期/有效期)
03
我们先看一个小例子:
fn main() { let a; { let b = 1; a = &b; } println!("{}", *a); }
这段代码是编译不通过的,来看下编译器给出的错误。
字面意思就是 b 活的不够长
,很通俗易懂哈哈。简单分析下:变量 a
是一个 &i32
引用类型,我们在内部代码块中初始化 a
,但是当内部代码块执行结束后,变量 b
离开作用域被释放了,但是 a
没有被释放,这时 a
就会变成 悬垂指针
,当然这在 Rust 中是绝对不允许的。
理论上来讲,其实所有的变量都存在生存期,变量的生命期一定是包含引用的生存期。先来看下面这张图片,红框所示的区域是变量b的生命期。蓝框所示的是a(b的引用)的作用域。很显然b的作用域没有包含a,变量的生命期没有包含引用的生命期,这种做法是禁止的。
我们转换下代码,如下图:
很显然b的作用域包含a,变量的生命期包含了引用的生命期。这段代码是可以正常编译的。
生命期的使用
04
标注生命期
只有引用类型才需要标注 lifetime。因此,以&i32
为例,标注生命期后变为 &'a i32
,在 &
后添加 'a
,通常叫做生命期 a
。a 可以被更换,其命名规则参考变量的命名规则。
&'a i32
标注生命期 a 的共享引用&'a mut i32
标注生命期 a 的可变引用
函数/方法签名中的生命期标注
编译器通常会推断生命期,当然我们也可以标注生命期。通常我们写函数/方法时是下面的写法。
fn test(name: &str) -> &str { println!("{}", name); return name; }
其实,这里是存在生命期标注的,如果编译器可以自动推断生命期时,则无需标注。上面的函数添加生命期标注后如下所示:
fn test_life<'_a>(name: &'_a str) -> &'_a str { println!("{}", name); return name; }
在函数名后面,添加<'a>
,如同泛型,在标注前先声明。然后再对每个参数或者返回值标注。
为什么存在生命期?
生命期仅用于编译器的检查。并不会更改原有生命期的长短。举个简单的例子,下面的代码是传入两个字符串,返回最长的那个字符串。
fn main() { let x = String::from("xxx"); let y = "yyyy"; let z = longest(x, y); println!("{}", z); } fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } }
如果我们直接编译,会提示错误。
error[E0106]: missing lifetime specifier --> src\main.rs:31:33 | 31 | 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 | 31 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { | ++++ ++ ++ ++ For more information about this error, try `rustc --explain E0106`. error: could not compile `lifetime` due to previous error
错误提示告诉我们,缺少生命期标识符。在当编译时期,rust 并不知道我们返回的是 x
还是 y
,因此不能确定返回的字符串的生命期。这个函数主体中, if
块返回的是 x
的引用,而 else
块返回的是 y
的引用。所以我们需要标注生命期,来告诉编译器。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
标注传入参数的生命期都是 a
,返回值的生命期也是 a
,所以无论返回 x
还是 y
,都是生命期 a
的 &str
。因此:生命期仅用于编译器的检查。
引用作为函数/方法返回值(接上节)
05
我们再看下面一个例子,编译会发现报错:
/// 拼接两个字符串 fn concat_str<'a>(x: &'a str, y: &'a str) -> &'a str { let s = format!("{}{}",x, y); return s.as_str(); }
下面是错误,不能返回一个局部变量:
error[E0515]: cannot return reference to local variable `s` --> src\main.rs:36:12 | 36 | return s.as_str(); | ^^^^^^^^^^ returns a reference to data owned by the current function For more information about this error, try `rustc --explain E0515`. error: could not compile `lifetime` due to previous error
我们思考,假设可以通过编译,会发生什么?当函数结束后,s
被释放,返回的引用会变成 悬垂引用
,这种做法是在 rust 中禁止的。因此我们可以得出一个结论,当从一个函数/方法返回一个引用时,返回类型的生命期参数需要与其中一个参数的生命期参数相匹配。当然也存在例外,继续往下看。
静态生命期
06
在 Rust 中,存在一种静态生命期 'static
。它表示数据在程序的整个运行期间都有效,它常用于储存全局静态数据和字符串常量。像一些字符串字面量,字节字符串字面量等等这些类似的生命期默认是 'static
。在函数里,可以直接返回 'static
的生命期。
fn get_any_str() -> &'static str { return "static"; }
小结
07
本章仅仅是简单了解下 lifetime,lifetime 是 Rust 中用来保证引用在使用时是有效的。生命期并不会改变在方法和函数中返回引用时,如果返回的引用不指向其中一个参数,那么它必须指向在这个函数中创建的一个值,然而这将会产生悬垂引用。本篇文章写的有些仓促,望大家见谅!