Rust 中的生命周期

7 min read

我们在讨论 Rust 的所有权时提到了一个非常重要的概念:生命周期 (lifetime),也就是引用保持有效的作用域。

这个概念与所有权结合的相当紧密,同时又与借用和引用关系很大,是属于又难啃又不得不啃的硬骨头。不过在理解后就会真真切切的体会到 Rust 中所有权、生命周期系统等一系列设计的精密。

在真正开始前,我们需要先了解一下 Rust 中的引用和借用。

引用和借用

引用和借用并不是 Rust 所发明的新概念。它在别的语言中也有:在 Java 中它也被称为引用 (有一点区别)、在 C++ 中它被称为指针,不过 Rust 的引用更安全。

我们先来看一个例子:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
  // 所以什么也不会发生

我们传递 &s1calculate_length,同时在函数定义中,我们获取 &String 而不是 String

这些 & 符号就是 引用,它们允许你使用值但不获取其所有权。

References

与使用 & 引用相反的操作是 解引用 (dereferencing),它使用解引用运算符,*

我们将获取引用作为函数参数称为 借用 (borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。

悬垂引用 (Dangling References)

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针 (dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

让我们尝试创建一个悬垂引用:

fn main() {
    let r;
    {
        let a = 1;
        r = &a;
    }
    println!("{}", r);
}

这是无法通过编译的:

error[E0597]: `a` does not live long enough
 --> src/main.rs:5:9
  |
5 |         r = &a;
  |         ^^^^^^ borrowed value does not live long enough
6 |     }
  |     - `a` dropped here while still borrowed
7 |     println!("{}", r);
  |                    - borrow later used here

上面代码中,当 a 离开作用域的时候会被释放,但此时 r 还持有一个 a 的借用,编译器中的借用检查器就会告诉你:`a` does not live long enough。翻译过来就是:a 活的不够久。这代表着 a 的生命周期太短,而无法借用给 r ,否则 &a 就指向了一个曾经存在但现在已不再存在的对象,这就是悬垂指针,也有人将其称为野指针。

生命周期

部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。如果你不指定生命周期,那么编译器会“强制”你这么做:

struct Foo {
    x: &i32,
}

fn main() {
    let y = &5;
    let f = Foo { x: y };

    println!("{}", f.x);
}
error[E0106]: missing lifetime specifier
 --> src/main.rs:2:8
  |
2 |     x: &i32,
  |        ^ expected lifetime parameter

error: aborting due to previous error

上面这段代码,编译器会提示你:missing lifetime specifier。这是因为,y 这个借用被传递到了 let f = Foo { x: y } 所在作用域中。所以需要确保借用 y 活得比 Foo 结构体实例长才行,否则,如果借用 y 被提前释放,Foo 结构体实例就会造成悬垂指针了。所以我们需要为其增加生命周期标记:

struct Foo<'a> {
    x: &'a i32,
}

fn main() {
    let y = &5;
    let f = Foo { x: y };

    println!("{}", f.x);
}

加上生命周期标记以后,编译器中的借用检查器就会帮助我们自动比对参数变量的作用域长度,从而确保内存安全。

再比如这个例子:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

因为 longest 的实例返回的是一个指针, 并且这个指针与 xy 都有关系。所以必须确保xy 活得比 result 长 (或者一样长),这便是显式生命周期注释的作用。

相关文章