Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1.11 生命周期(进阶) Pt.1:回顾、借用检查器、泛型生命周期

这篇文章在已有基础上对生命周期这一概念进行补充。

1.11.1. 回顾

在初级教程中我们提到过:Rust里每个引用都有生命周期,它就是引用保持合法的作用域(scope),大多数时候都是隐式并且由编译器推断出来的。

对某个变量取得引用时生命周期开始,当变量移动或离开作用域时生命周期结束。也就是对于某个引用来说,它必须保持合法的一个代码区域的名称。

生命周期通常与作用域重合,但也不一定。

1.11.2. 借用检查器(Borrow Checker)

每当具有某个生命周期'a的引用被使用,借用检查器都会检查'a是否还存活。具体方法是:

  • 追踪路径到'a开始(获得引用)的地方
  • 从这开始,检查沿着路径是否存在冲突
  • 保证引用指向一个可安全访问的值

这个例子使用了 rand crate。在Cargo.toml中添加如下依赖:

[dependencies]
rand = "0.8"

看个例子:

use rand::random;  
  
fn main() {  
    let mut x = Box::new(42);  
    let r = &x;  
    if random::<f32>() > 0.5 {  
        *x = 84;  
    } else {  
        println!("{}", r);  
    }  
}
  • xBox<i32>类型

  • r声明为x的引用,从这一行(第5行)开始引用的生命周期就开始了

  • 第7行对x进行了解引用修改值的操作,需要一个指向x的可变引用。借用检查器此时就会去出一个指向x的可变引用,并检查它的使用是否存在冲突,这个代码例中没有冲突,所以代码是合法的

  • 有人可能会问了:第7行在r的作用域里,*x需要指向x的可变引用,那在同一个作用域里既有不可变引用r和可变引用*x违反了借用规则不应该报错吗? 事实上Rust很聪明,知道代码如果走了if分支就不能走else分支了,rif分支下根本就没有使用过,所以在if分支下使用*x这个可变引用是没有问题的。换句话说,r的生命周期并没有延伸到if分支里。这就是生命周期通常不与作用域完全重合的例子

再看一个例子:

fn main() {  
    let mut x = Box::new(42);  
      
    let mut z = &x;  
    for i in 0..100 {  
        println!("{}", z);  
        x = Box::new(i);  
        z = &x;  
    }  
    println!("{}", z);  
}
  • xBox<i32>类型
  • zx的引用,从这行(第4行)生命周期就开始了
  • 第6行,循环中打印了z,使用到了z这个引用自然就会被借用检查器检查。这里没有什么问题,所以借用检查器不会报错
  • 第7行,x被重新赋值
  • 第8行,z被重新赋值,Rust会把新赋的这个引用视作是另一个引用,所以相当于第8行是新的生命周期,而原本的生命周期到7行就结束了
  • 之后的每次循环都是z = &x;这一行会开启一个新的生命周期。借用检查器因此不会报错

借用检查器的特性

借用检查器是保守的:如果不确定某个借用是否合法,借用检查器就会拒绝该借用。

借用检查器有时候会需要帮助来理解借用为什么是合法的,这就是Unsafe Rust存在的部分原因。

1.11.3. 泛型生命周期

有时候我们需要在自己的类型里储存引用。我们就需要给这些引用标注生命周期,以便借用检查器检查合法性。例如在类型方法中返回引用,且存活时间比self长。

Rust允许你基于一个或多个生命周期将类型的定义泛型化。

两点提醒

  • 如果类型实现了Drop trait,那么丢弃类型时,就被记作是使用了类型所泛型的生命周期或类型。如果类型没有实现Drop trait,那么类型丢弃时不会当作使用了生命周期,可以忽略类型内的引用。 举个例子:某个类型实例要被丢弃了,在丢弃之前,借用检查器会查看是否仍然合法地去使用你类型的泛型生命周期,因为你在drop函数中的代码可能会用到这些引用

  • 类型可泛型多个生命周期,但通常没必要让类型签名变得更复杂。只有类型包含多个引用时,你才应该使用多个生命周期参数,并且返回的引用只应绑定到其中一个引用的生命周期。

看个例子:

#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {  
    if x.len() > y.len() {  
        x  
    } else {  
        y  
    }  
}
}

'a表示有a这么一个生命周期,xy以及返回类型都是这个生命周期a,这个时候就表示xy和返回类型的生命周期是一样的。