之前学习 Rust 的时候,遇到生命周期相关的内容,总是喜欢先跳过,写代码的时候,结构体一律使用 String 类型代替 &str,然而生命周期相关概念,在 Rust 中非常重要,终于还是抽了两天晚上的时间学习,有所收获~
生命周期与引用
如果多个指针指向一个变量,变量被一个指针释放后其它指针就会成为悬垂指针,产生内存安全问题。
Rust 在编译期检查,保障内存安全,就是要提前发现这一错误,以函数举例,传一个字符串进去,返回这个字符串的引用,那么就要保障在返回的引用的生命周期内,传入字符串都要是有效的,否则传入字符串比引用早销毁,就产生了悬垂指针,Rust 编译器是绝对不会允许的。
函数生命周期标注
一般情况下,Rust 能够自动推导出变量的生命周期,无需我们手动进行标注,但是一些场景下,编译器无法通过数学关系推断出我们程序逻辑的时候,需要我们手动进行生命周期标注,供编译器使用。
fn foo(s: &str) -> &str {
s
}
fn main() {
let s = "hello";
println!("{}", foo(&s));
}
这是一段没有什么用但很简单的代码,foo() 函数将传入的字符串原封不动的返回,编译运行,没有报错。
稍微进行一些改动,将 foo() 函数参数变为两个,并且返回二者较大值,编译将会报错。
fn foo(x: &str, y: &str) -> &str {
if x > y {
x
} else {
y
}
}
fn main() {
let x = "a";
let y = "b";
println!("{}", foo(&x, &y));
}
报错信息如下
error[E0106]: missing lifetime specifier
--> src/bin/b.rs:3:29
|
3 | fn foo(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
|
3 | fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
| ^^^^ ^^^^^^^ ^^^^^^^ ^^^
error: aborting due to previous error
For more information about this error, try `rustc --explain E0106`.
error: could not compile `hello`.
提示我们缺少声明周期标注。
为什么呢?
在 Rust 的函数中,如果返回值与传入值相关,那么返回值的生命周期需是传入参数生命周期交集的子集。
可以这样做来消除上边的编译错误
fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
if x > y {
x
} else {
y
}
}
fn main() {
let x = "a";
let y = "b";
println!("{}", foo(&x, &y));
}
在函数名的后边的尖角括号中声明了一个生命周期标识 'a
,你可以起任何名称,如:'b
,'hello_world
它只是一个代号。函数参数列表 x 和 y 后边同时也声明了参数符合 'a
生命周期的约束,即在生命周期 'a
内 x 值都有效,同理,返回值也符合 'a
生命周期约束,'a
⊆ 'a ∩ 'a
显然正确,可以正确编译。
再看一个例子
fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
"hello"
}
fn main() {
let x = "a";
let y = "b";
println!("{}", foo(&x, &y));
}
本例中的返回值是 'static str'
类型的值 hello,它在程序运行的生命周期内都是有效的,跟传入参数的生命周期无关,所以可以编译成功。
目前遇到的例子,生命周期标注都是一样的 'a
,如果传入参数的生命周期不同,则编译期间会报错。
fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
if x > y {
x
} else {
y
}
}
fn main() {
let s1 = "a";
let s2 = "b";
println!("{}", foo(&s1, &s2));
}
在上边的例子的函数中,声明了两个生命周期标识符,'a
与 'b
,分别描述了 x, y 的生命周期。
error[E0623]: lifetime mismatch
--> src/bin/e.rs:7:9
|
3 | fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
| ------- -------
| |
| this parameter and the return type are declared with different lifetimes...
...
7 | y
| ^ ...but data from `y` is returned here
编译器重拳出击,告诉我们生命周期没描述清楚,它无法自动推导出在 y 值的生命周期下,返回值都有效的结论。
也就是它无法判断 'a
⊆ 'a ∩ 'b
是否正确,所以我们需要多进行一些标注,告诉编译器 'a
是 'b
的子集。
fn foo<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {}
此处的 'b: 'a
表达的意思就是:“'a
是 'b
的子集,如果变量符合 'b
约束,那么它一定符合 'a
的生命周期约束”。
有点类似于:“小明如果能打100分,那他一定也能打60分,如果他最高只能打到60分,他一定打不了100分”。
添加标注后编译通过,'a
⊆ 'a ∩ 'b
,满足编译条件。
结构体生命周期标注
如果在结构体中使用引用,则需要显示进行生命周期标注。
struct Foo {
x: &i32
}
fn main() {
let y = &5;
let f = Foo { x: y };
println!("{}", f.x);
}
以上代码编译将会报错,因为编译器不知道结构体实例与其引用的变量谁先被销毁,只有当实例的生命周期 ⊆ 变量的生命周期时,才能通过编译器检查。
struct Foo<'a> {
x: &'a i32
}
fn main() {
let y = &5;
let f = Foo { x: y };
println!("{}", f.x);
}
添加生命周期标注后,编译通过。
借助 impl
,我们可以为结构体实现方法。
struct Foo<'a> {
x: &'a i32
}
impl<'a> Foo<'a> {
fn x(&self) -> &'a i32 { self.x }
}
fn main() {
let y = &5;
let f = Foo { x: y };
println!("{}", f.x());
}
正如函数名后面的 <'a>
生命周期标识符声明,impl
关键字后边的 <'a>
也起到声明作用,在 impl
代码块内使用,在方法上进行生命周期标注和在函数上就别无二致了~
这是就是基本的生命周期声明与标注了,在 Rust 中,生命周期发散开来还有很多的知识点~
这篇《Rust生命周期的常见误解》英文 中文翻译 就很值得一看,我阅读的时候,有些地方 Rust 语法还不太熟悉,就先把自己了解的部分梳理一下,Rust 的生命周期是贯穿 Rust 语言始终的,后续还是要再深入学习才好。
参考
- https://rustcc.gitbooks.io/rustprimer/content/ownership-system/lifetime.html
- https://doc.rust-lang.org/1.9.0/book/lifetimes.html