Skip to content

Latest commit

 

History

History
130 lines (95 loc) · 7.73 KB

README.md

File metadata and controls

130 lines (95 loc) · 7.73 KB
title tags
Lifetime
Rust
basic
wtfacademy

WTF Rust 极简入门: 生命周期

在 Rust 中,生命周期是一个非常重要的概念,用于确保引用不会悬空,即引用的数据在引用存在的时间里始终有效。Rust 编译器通过生命周期来检查这种有效性。这一节将讨论生命周期的重要性、如何使用生命周期注解,以及它如何帮助我们写出更安全的代码。

悬空引用

在第 5 节我们知道,Rust 中所有的对象都是有主人(owner)的,它拥有对象的所有权(ownership),所有权具有唯一性,但很多时候我们并不需要对象的所有权,有使用权就够了,而使用权在 Rust 中称之为引用(borrow),或者说借用。

let s1 = String::from("hello");
let s2 = &s1;

在上面的例子中,s1 拥有 hello 这个对象的所有权,而 s2 通过 & 修饰符表示引用这个对象,默认是不可变引用。这是一个正常的例子,但在有些情况下,引用会变得无效:

let s2;
{
    let s1 = String::from("hello");
    s2 = &s1;
}
println!("s2: {}", s2);

上面的例子中,s2 引用的 s1 对象的作用域在花括号内部,超出这个作用域之后,s1 这个对象会被 Rust 自动回收,此时再去打印 s2 的值就会发生错误:s1 does not live long enough, borrowed value does not live long enough,即对象 s1 存活的时间不够长,导致 s2 引用了一个不存在的值,这种情况,我们称之为悬空引用

生命周期基础

Rust 中的每一个引用都有其生命周期(lifetime),也就是引用保持有效的作用域。生命周期在 Rust 中的核心作用是防止悬空引用的发生,它是许多程序错误和安全隐患的根源。生命周期确保内存安全,无需垃圾收集。

在大多数时候,我们无需手动的声明生命周期,因为编译器可以自动进行推导,但当多个生命周期存在时,编译器可能无法进行引用的生命周期分析,就需要我们手动标明不同引用之间的生命周期关系,也就是生命周期注解。

生命周期注解语法

生命周期参数名称必须以撇号'开头,其名称通常全是小写,类似于泛型其名称非常短。'a 是默认使用的名称。生命周期参数标注位于引用符 & 之后,并有一个空格来将生命周期注解与引用的类型分隔开,如 &'a i32

生命周期注解并不改变任何引用的生命周期的长短。它只是描述了多个引用生命周期相互的关系,便于编译器进行引用的分析,但不影响其生命周期。

生命周期即 Rust 中值的生老病死,而生命周期注解就是用于约定多个引用之间生死的关系,就像桃园三结义中誓约的那样:不求同年同月同日生,但求同年同月同日死,这三兄弟之间的誓约就如同 Rust 中的生命周期注解。誓约是为了防止有人背信弃义,而 Rust 生命周期注解是为了编译器进行分析,防止出现悬垂引用。有了誓约并不代表大家就一定一起赴死,就如同生命周期注解并不改变值的生命周期一样。

fn borrow<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
    if x > y { x } else { y }
}

这个函数 borrow 接收两个引用参数,并返回一个它们中的一个。生命周期注解 'a 指示这两个输入引用和返回引用必须拥有相同的生命周期。

生命周期在结构体中的应用

生命周期在结构体定义中尤其重要,尤其是当结构体要包含对某种数据的引用时。

struct Book<'a> {
    title: &'a str,
    pages: i32,
}

fn main() {
    let title = String::from("Rust Programming");
    let book = Book {
        title: &title,
        pages: 384,
    };

    println!("Book: {} - {} pages", book.title, book.pages);
}

在这个例子中,Book 结构体有一个生命周期注解 'a,这意味着字段 title 的生命周期至少要和 Book 实例一样长,否则就会发生悬空引用。

生命周期省略规则(Lifetime Elision Rules)

在某些常见情况下,Rust 允许省略生命周期注解。编译器遵循一组特定的规则(称为生命周期省略规则),在这些规则适用的情况下,可以推断出引用的生命周期。

  1. 每个引用参数都有自己的生命周期参数。
  2. 如果只有一个输入生命周期参数,该生命周期被赋给所有输出生命周期参数。
  3. 如果有多个输入生命周期参数,但其中之一是 &self&mut self(说明是方法),则 self 的生命周期被赋给所有输出生命周期参数。

显式生命周期在复杂场景中的应用

考虑到更复杂的场景,显式生命周期注解变得尤为重要。它能确保代码在引用和数据管理方面的正确性,特别是在多个不同生命周期和复杂数据类型交互时。

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

这个函数 longest 涉及到了两个不同的生命周期注解 'a'b,而 'b: 'a 表示输入参数 y 的生命周期至少与输入参数 x 相同,或比它更长。函数返回值中 'a 表示返回的引用将具有与输入参数 x 相同的生命周期,也就是生命周期中最小的那个。这样可能比较抽象,我们看下具体的例子:

fn main() {
    let string1 = String::from("abcdefghijklmnopqrstuvwxyz");
    {
        let string2 = String::from("123456789");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

上面的代码展示了 string1string2 这两个不同生命周期的变量,前者的生命周期位于外部的 {} 中,而后者的生命周期位于内部的 {} 中,所以 'a 代表的生命周期范围是两者中较小的那个,即内部的 {},此时返回值 result 的生命周期也是属于内部的 {},即返回值能够保证在 string1string2 中较短的那个生命周期结束前有效,此时不会发生悬垂引用,编译通过。

接下来,让我们尝试另外一个例子,该例子揭示了 result 的引用的生命周期必须是两个参数中较短的那个。

fn main() {
    let string1 = String::from("abcdefghijklmnopqrstuvwxyz");
    let result;
    {
        let string2 = String::from("123456789");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

此时 'a 代表的生命周期范围依旧是变量 string1string2 中最小的那个,即内部的 {},但返回值 result 的生命周期范围却是外部的 {},而不是内部的 {},也就意味着 result 可能会引用一个无效的值,因此编译失败。

注意:通过人为观察 result 的引用应该为 string1,这样返回值 resultstring1 的作用域是一致的,理论上是应该编译通过的。但是,Rust 的编译器会采用保守的策略,我们通过生命周期标注告诉 Rust,longest 函数返回值的生命周期是传入参数中较小的那个变量的生命周期,因此 Rust 编译器不允许上述代码通过,因为可能存在无效引用。

总结

理解和正确使用生命周期是掌握 Rust 的重要部分。生命周期注解帮助 Rust 编译器保证引用的有效性,从而让你的程序在处理引用时更加安全。虽然开始时可能会觉得生命周期有些复杂,但随着实践的深入,你会逐渐领会它们的重要性和用法。掌握生命周期让你能写出更健壮、安全的 Rust 代码。如果你有任何问题或需要更多例子,请随时提问!