Android程序员初学Rust-生命周期

97 阅读5分钟

1.png

生命周期是编译器(更确切地说是其借用检查器)用于确保所有借用操作有效的一种机制。具体而言,变量的生命周期从其创建时开始,到其被销毁时结束。虽然生命周期和作用域常被一起提及,但它们并不相同。

引用有生命周期,其生命周期不能比它所引用的值的生命周期更长。这一点由借用检查器进行验证。

引用才与生命周期强相关

生命周期可以是隐式的——这是我们目前已经见到过的情况。生命周期也可以是显式的:如 &'a Point&'document str。生命周期以 ' 开头,'a 是典型的默认名称。把 &'a Point 理解为 “一个借用的 Point,其至少在生命周期 a 内有效”。

只有所有权(而非生命周期注解)控制着何时销毁值,并确定给定值的具体生命周期。借用检查器只是验证借用操作永远不会超出值的具体生命周期。

与类型一样,函数签名上需要显式的生命周期注解(不过在常见情况下可以省略)。这些注解为调用点和函数体内的类型推断提供信息,帮助借用检查器完成其工作。

#[derive(Debug)]
struct Point(i32, i32);

// compile error: expected named lifetime parameter
fn left_most(p1: &Point, p2: &Point) -> &Point { 
    if p1.0 < p2.0 { p1 } else { p2 }
}

fn main() {
    let p1: Point = Point(10, 10);
    let p2: Point = Point(20, 20);
    let p3 = left_most(&p1, &p2); // p3 的生命周期应该是如何的?
    dbg!(p3);
}

在这个例子中,编译器不知道该为 p3 推断出什么样的生命周期。查看函数体内部可以发现,它只能安全地假设 p3 的生命周期是 p1p2 中较短的那个。但是,就像类型一样,Rust 要求在函数参数和返回值上显式标注生命周期。

left_most 函数适当地添加 'a

fn left_most<'a>(p1: &'a Point, p2: &'a Point) -> &'a Point {

这表明存在某个生命周期 'ap1p2 的生命周期都比 'a 长,且返回值的生命周期也比 'a 短。借用检查器会在函数体内验证这一点,并在 main 函数中使用这个信息来确定 p3 的生命周期。

如果函数涉及多个生命周期,语法类似:

foo<'a, 'b>
// foo 具有生命周期参数 'a 和 'b

函数调用中的生命周期

虽然必须完整指定函数参数和返回值的生命周期,但在大多数情况下,Rust 允许依据一些简单规则省略生命周期标注。这并非类型推断,而仅仅是一种语法简写形式。

  • 每个没有标注生命周期的参数都会自动获得一个生命周期标注。

  • 如果只有一个参数带有生命周期标注,那么所有没有标注生命周期的返回值也会采用这个生命周期标注。

  • 如果存在多个参数的生命周期标注,且第一个标注是 self 的生命周期,那么所有没有标注生命周期的返回值都会采用 self 的这个生命周期标注。

#[derive(Debug)]
struct Point(i32, i32);

fn cab_distance(p1: &Point, p2: &Point) -> i32 {
    (p1.0 - p2.0).abs() + (p1.1 - p2.1).abs()
}

fn find_nearest<'a>(points: &'a [Point], query: &Point) -> Option<&'a Point> {
    let mut nearest = None;
    for p in points {
        if let Some((_, nearest_dist)) = nearest {
            let dist = cab_distance(p, query);
            if dist < nearest_dist {
                nearest = Some((p, dist));
            }
        } else {
            nearest = Some((p, cab_distance(p, query)));
        };
    }
    nearest.map(|(p, _)| p)
}

fn main() {
    let points = &[Point(1, 0), Point(1, 0), Point(-1, 0), Point(0, -1)];
    let nearest = {
        let query = Point(0, 2);
        find_nearest(points, &Point(0, 2))
    };
    println!("{:?}", nearest);
}

// Output
// Some(Point(1, 0))

在这个例子中,cab_distance 的生命周期标注可以轻松省略。

nearest 函数提供了另一个例子,其参数中有多个引用,因此需要显式标注生命周期。在 main 函数中,返回值的生命周期被允许长于查询对象的生命周期。

如果修改签名,在返回的生命周期上 “作假”:

fn find_nearest<'a, 'q>(points: &'a [Point], query: &'q Point) -> Option<&'q Point> {

这段代码将无法编译,这表明编译器会检查这些标注是否有效。请注意,原始指针(unsafe 类型)并非如此,这是 unsafe Rust 中常见的错误来源。

Rust 中的借用总是带有生命周期。大多数情况下,省略标注和类型推断意味着无需将这些生命周期写出来。在更复杂的情况下,生命周期标注有助于消除歧义。通常,尤其是在进行原型设计时,通过在必要时克隆值来处理拥有所有权的数据会更加容易。

结构体中的生命周期

如果一个数据类型存储了借用的数据,那么它必须标注一个生命周期:

#[derive(Debug)]
enum HighlightColor {
    Pink,
    Yellow,
}

#[derive(Debug)]
struct Highlight<'document> {
    slice: &'document str,
    color: HighlightColor,
}

fn main() {
    let doc = String::from("The quick brown fox jumps over the lazy dog.");
    let noun = Highlight { slice: &doc[16..19], color: HighlightColor::Yellow };
    let verb = Highlight { slice: &doc[20..25], color: HighlightColor::Pink };
    // drop(doc);
    dbg!(noun);
    dbg!(verb);
}

上述代码输出:

// Output
[src/main.rs:18:5] noun = Highlight {  
slice: "fox",  
color: Yellow,  
}  
[src/main.rs:19:5] verb = Highlight {  
slice: "jumps",  
color: Pink,  
}

在上述示例中,Highlight 上的标注强制要求所包含的 &str 的底层数据的生命周期,至少和使用该数据的任何 Highlight 实例的生命周期一样长。结构体的生命周期不能长于它所引用的数据的生命周期。

如果 docnounverb 的生命周期结束之前被释放,借用检查器会抛出一个错误。

包含借用数据的类型强制用户保留原始数据。这对于创建轻量级视图可能很有用,但通常会使它们在某种程度上更难使用。

尽可能让数据结构直接拥有自己的数据。

一些内部有多个引用的结构体可能有多个生命周期标注。如果除了结构体本身的生命周期之外,还需要描述引用之间的生命周期关系,那么这可能是必要的。不过这些都是非常高级的用例,教程后续会提到。