Rust 生命周期初探

Published: 2020-07-29

Tags: Rust


之前学习 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 语言始终的,后续还是要再深入学习才好。

参考

  1. https://rustcc.gitbooks.io/rustprimer/content/ownership-system/lifetime.html
  2. https://doc.rust-lang.org/1.9.0/book/lifetimes.html