Appearance
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”。
以下就是省略的生命周期被自动补全的规则:
编译器会为每个引用参数分配一个生命周期参数;换句话说,具有一个参数的函数获得一个生命周期参数:
fn foo<'a>(x: &'a i32);
,具有两个参数的函数获得两个独立的生命周期参数:fn foo<'a, 'b>(x: &'a i32, y: &'b i32);
等等。如果只有一个输入参数带生命周期参数,那么返回值的生命周期被指定为这个参数:
fn foo<'a>(x: &'a i32) -> &'a i32
如果有多个输入参数带生命周期参数,但其中有
&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 会应用第一个生命周期省略的规则,给出 &self
和 announcement
各自的生命周期,而第二个规则不满足,跳过。最后,因为其中一个参数是 &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
,省略生命周期参数和类型自动推导的原理是完全不同的。