Rust 生命周期

Rust在整个生命周期里强制执行生命周期的规则。生命周期说白了就是作用域的名字。每一个引用以及包含引用的数据结构,都要有一个生命周期来指定它保持有效的作用域。

在函数体内,Rust通常不需要你显式地给生命周期起名字。这是因为在本地上下文里,一般没有必要关注生命周期。Rust知道程序的全部信息,从而可以完美地执行各种操作。它可能会引入许多匿名或者临时的作用域让程序顺利执行。

但是如果你要跨出函数的边界,就需要关心生命周期了。生命周期用这样的符号表示:'a,'static。为了更清晰地了解生命周期,我们假设我们可以为生命周期打标签,去掉本章所有例子的语法糖。

最开始,我们的示例代码对作用域和生命周期使用了很激进的语法糖特性——甜得像玉米糖浆一样,因为把所有的东西都显式地写出来实在很讨厌。所有的Rust代码都采用比较激进的理论以省略“显而易见”的东西。

一个特别有意思的语法糖是,每一个let表达式都隐式引入了一个作用域。大多数情况下,这一点并不重要。但是当变量之间互相引用的时候,这就很重要了。举个简单的例子,我们彻底去掉下面这段代码的语法糖:

let x = 0;
let y = &x;
let z= &y;

借用检查器通常会尽可能减少生命周期的范围,所以去掉语法糖后的代码大概像这样:

// 注意:'a: { 和 &'b x 不是合法的语法
'a: {
    let x: i32 = 0;
    'b: {
        // 生命周期是'b,因为这就足够了
        let y: &'b i32 = &'b x;
        'c: {
            // 'c也一样
            let z: &'c &'b i32 = &'c y;
        }
    }
}

哇!这样的写法……太可怕了。我们先停下来感谢Rust把这一切都简化掉了。

将引用传递到作用域以外会导致生命周期扩大:

let x = 0;
let z;
let y = &x;
z = y;
'a: {
    let x: i32 = 0;
    'b: {
        let z: &'b i32;
        'c: {
            // 必须使用'b,因为引用被传递到了'b的作用域
            let y: &'b i32 = &'b x;
            z = y;
        }
    }
}

示例:引用超出被引用内容生命周期

好了,让我们再看一遍曾经举过的一个例子:

fn as_str(data: &u32) -> &str {
    let s = format!("{}", data);
    &s
}

去掉语法糖:

fn as_str<'a>(data: &'a u32) -> &'a str {
    'b: {
        let s = format!("{}", data);
        return &'a s;
    }
}

函数as_str的签名里接受了一个带有生命周期的u32类型的引用,并且保证会返回一个生命周期一样长的str类型的引用。从这个签名我们就已经可以看出问题了。它表示我们必须到那个u32引用的作用域,或者比它还要早的作用域里去找一个str。这就有点不合理了。

接下来我们生成一个字符串s,然后返回它的引用。我们的函数要求这个引用的有效期不能小于'a,那是我们给引用指定的生命周期。不幸的是,s是在作用域'b里面定义的。除非'b包含'a这个函数才可能是正确的——而这显然不可能,因为'a必须包含它所调用的函数。这样我们创建了一个生命周期超出被引用内容的引用,这明显违背了之前提到的引用的第一条规则。编译器十分感动然后拒绝了我们。

我们扩展一下这个例子,一边看得更清楚:

fn as_str<'a>(data: &'a u32) -> &'a str {
    'b: {
        let s = format!("{}", data);
        return &'a s;
    }
}

fn main() {
    'c: {
        let x: u32 = 0;
        'd: {
            // 这里引入了一个匿名作用域,因为借用不需要在整个x的作用域内生效
            // as_str的返回值必须引用一个在函数调用前就存在的str
            // 显然事实不是这样的。
            println!("{}", as_str::<'d>(&'d x));
        }
    }
}

完蛋了!

当然,这个函数的正确写法应该是这样的。

fn to_string(data: &u32) -> String {
    format!("{}", data)
}

我们必须创建一个值然后连同它的所有权一起返回。除非一个字符串是&'a u32的成员,我们才能返回&'a str,显然事情并不是这样的。

(其实我们也可以返回一个字符串的字面量,它是一个全局的变量,可以认为是处于栈的底部。尽管这样极大限制了函数的使用场合。)

示例:存在可变引用的别名

在看另一个老的例子:

let mut data = vec![1, 2,3];
let x = &data[0];
data.push(4);
println!("{}", x);
'a: {
    let mut data: Vec<i32> = vec![1, 2, 3];
    'b: {
        // 对于这个借用来说,'b已经足够大了
        // (借用只需要在println!中生效即可)
        let x: &'b i32 = Index::index::<'b>(&'b data, 0);
        'c: {
            // 引入一个临时作用域,因为&mut不需要存在更长时间
            Vec::push(&'c mut data, e);
        }
        println!("{}", x);
    }
}

这里的问题更加微妙也更有趣。我们希望Rust出于如下的原因拒绝编译这段代码:我们有一个有效的指向data的内部数据的引用x,而同时又创建了一个data的可变引用用于执行push。也就是说出现了可变引用的别名,这违背了引用的第二条规则。

但是Rust其实并非因为这个原因判断这段代码有问题。Rust不知道xdata的子内容的引用,它其实完全不知道Vec的内部是什么样子的。它只知道x必须在'b范围内有效,这样才能打印其中的内容。函数Index::index的签名因此要求传递的data的引用也必须在'b的范围内有效。当我们调用push的时候,Rust发现我们要创建一个&'c mut data。它知道'c是包含在'b以内的,因为&'b data还存活着,所以它拒绝了这段程序。

我们看到了生命周期系统要比引用的保护措施更加简单粗暴。大多数情况下这也没什么,它让我们不用没完没了地向编译器解释我们的程序。但是这也意味着许多语义上正确的程序会被编译器拒绝,因为生命周期的规则太死板了。

考虑下面的代码:struct Foo;impl Foo { fn mutate_and_share(&mut self) -> &Self {&*self} fn share(&a ...