Skip to content

12.5 省略生命周期标记

在某些情况下,Rust 允许我们在写函数的时候省略掉显式生命周期标记。 在这种时候,编译器会通过一定的固定规则为参数和返回值指定合适的生命周期,从而省略一些显而易见的生命周期标记。 比如我们可以写这样的代码:

rust
fn get_str(s: &String) -> &str {
    s.as_ref()
}

实际上,它等同于下面这样的代码,只是把显式的生命周期标记省略掉了而已:

rust
fn get_str<'a>(s: &'a String) -> &'a str {
    s.as_ref()
}

若把以上代码稍微修改一下,返回的指针将并不指向参数传入的数据,而是指向一个静态常量,这时,我们期望返回的指针实际上是&'static str类型。测试代码如下:

rust
fn get_str(s: &String) -> &str {
    println!("call fn {}", s);
    "hello world"
}

fn main() {
    let c = String::from("haha");
    let x: &'static str = get_str(&c);
    println!("{}", x);
}

可以看到,在get_str函数中,返回的是一个指向静态字符串的指针。在主函数的调用方,我们希望变量x指向一个“静态变量”。可是这一次,我们发现了编译错误:

error[E0597]: `c` does not live long enough

按照分析,变量x理应指向一个'static生命周期的变量,根本不是指向c变量,它的存活时间足够长,为什么编译器没发现这一点呢? 这是因为,编译器对于省略掉的生命周期,不是用的“自动推理”策略,而是用的几个非常简单的“固定规则”策略。 这跟类型自动推导不一样,当我们省略变量的类型时,编译器会试图通过变量的使用方式推导出变量的类型,这个过程叫“type inference”。

而对于省略掉的生命周期参数,编译器的处理方式就简单粗暴得多,它完全不管函数内部的实现,并不尝试找到最合适的推理方案,只是应用几个固定的规则而已, 这些规则被称为“lifetime elision rules”。

以下就是省略的生命周期被自动补全的规则:

  1. 编译器会为每个引用参数分配一个生命周期参数;换句话说,具有一个参数的函数获得一个生命周期参数:fn foo<'a>(x: &'a i32);,具有两个参数的函数获得两个独立的生命周期参数:fn foo<'a, 'b>(x: &'a i32, y: &'b i32);等等。

  2. 如果只有一个输入参数带生命周期参数,那么返回值的生命周期被指定为这个参数:fn foo<'a>(x: &'a i32) -> &'a i32

  3. 如果有多个输入参数带生命周期参数,但其中有&self&mut self,那么这个self的生命周期被分配给所有输出生命周期参数。第三条规则使方法更易于读写,因为所需的符号更少。

第三条规则实际上只适用于方法签名,所以通常我们不需要在方法签名中注释生命周期。

rust
impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

上面示例,因为有两个输入生命周期,所以 Rust 会应用第一个生命周期省略的规则,给出 &selfannouncement 各自的生命周期,而第二个规则不满足,跳过。最后,因为其中一个参数是 &self,最终该寒暑的返回类型获得 &self 的生命周期,并且所有生命周期都已经考虑在内了。

如果以上都不满足,就不能自动补全返回值的生命周期参数。

我们通常会把函数或方法参数的生命周期称为输入生命周期(input lifetimes),返回值的生命周期称为输出生命周期(output lifetimes)。

接着,让我们看一个例子,你就会明白为啥下面的例子会报错。

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

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

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

假如我们现在就是编译器,那么 longest 函数,我们会应用上面的三个规则来自动补全生命周期参数:

开始应用第一个规则,该规则指定每个参数都有自己的生存期。我们将像往常一样称之为 'a ,所以现在的签名是这样的:

rust
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

因为存在多个输入生存期,所以第二条规则不适用。 第三条规则也不适用,因为 longest 是一个函数而不是一个方法,所以没有一个参数是 self

在完成了所有三个规则之后,我们仍然没有弄清楚返回类型的生存期是什么。所以这里的代码在编译时会出现错误:编译器通过了生存期省略规则,但仍然无法计算出签名中引用的所有生存期。

现在这个函数签名中的所有引用都有了生命周期,编译器可以继续往下进行其他分析,而不需要程序员在这个函数签名中注释生命周期。

这时再回头去看前面的例子,可以知道,对于这个函数:

rust
fn get_str(s: &String) -> &str {
    println!("call fn {}", s);
    "hello world"
}

自动补全生命周期参数之后:

rust
fn get_str<'a>(s: &'a String) -> &'a str {
    println!("call fn {}", s);
    "hello world"
}

所以,当我们调用

rust
let x: &'static str = get_str(&c);

这句代码的时候,就发生了编译错误。了解了这些,修复方案也就很简单了。在这种情况下,我们不能省略生命周期参数,让编译器给我们自动补全,自己手写就对了:

rust
fn get_str<'a>(s: &'a String) -> &'static str {
    println!("call fn {}", s);
    "hello world"
}

或者只手写返回值的生命周期参数,输入参数靠编译器自动补全:

rust
fn get_str(s: &String) -> &'static str {
    println!("call fn {}", s);
    "hello world"
}

fn main() {
    let c = String::from("haha");
    let x: &'static str = get_str(&c);
    println!("{}", x);
}

最后,一句话总结,elision!=inference,省略生命周期参数和类型自动推导的原理是完全不同的。

参考

lifetime elision

Released under the MIT License