我正在阅读Rust书的生命周期章节,我看到了一个命名/显式生命周期的例子:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let x; // -+ x goes into scope
// |
{ // |
let y = &5; // ---+ y goes into scope
let f = Foo { x: y }; // ---+ f goes into scope
x = &f.x; // | | error here
} // ---+ f and y go out of scope
// |
println!("{}", x); // |
} // -+ x goes out of scope
我很清楚,编译器阻止的错误是分配给x的引用的use-after-free:内部作用域完成后,f和&f。X变得无效,并且不应该分配给X。
我的问题是,这个问题可以很容易地分析掉,而不使用显式的` a lifetime,例如,通过推断一个更广泛的范围的引用的非法赋值(x = &f.x;)。
在哪些情况下,实际上需要显式生命期来防止free后使用(或其他类?)错误?
让我们来看看下面的例子。
fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
x
}
fn main() {
let x = 12;
let z: &u32 = {
let y = 42;
foo(&x, &y)
};
}
在这里,显式生命期很重要。这是因为foo的结果与它的第一个参数('a)具有相同的生命期,所以它可能比它的第二个参数更长寿。这是由foo签名中的生存期名称表示的。如果你在foo调用中切换参数,编译器会抱怨y的生存时间不够长:
error[E0597]: `y` does not live long enough
--> src/main.rs:10:5
|
9 | foo(&y, &x)
| - borrow occurs here
10 | };
| ^ `y` dropped here while still borrowed
11 | }
| - borrowed value needs to live until here
其他答案都有突出的要点(fjh的具体例子中需要显式生命期),但忽略了一个关键问题:为什么编译器会告诉你你弄错了,还需要显式生命期?
这实际上和“编译器可以推断显式类型,为什么还需要显式类型”是同一个问题。一个假设的例子:
fn foo() -> _ {
""
}
当然,编译器可以看到我返回一个&'静态str,那么为什么程序员必须键入它呢?
主要原因是,虽然编译器可以看到你的代码做了什么,但它不知道你的意图是什么。
函数是防止代码更改影响的天然边界。如果我们允许从代码中完全检查生存期,那么一个看似无害的更改可能会影响生存期,这可能会在遥远的函数中导致错误。这不是一个假设的例子。据我所知,当对顶级函数依赖类型推断时,Haskell就会遇到这个问题。Rust将这个问题扼杀在萌芽状态。
对于编译器来说,还有一个效率上的好处——只需要解析函数签名来验证类型和生存期。更重要的是,它可以提高程序员的效率。如果我们没有显式的生存期,这个函数会做什么:
fn foo(a: &u8, b: &u8) -> &u8
不检查源代码是不可能知道的,这将违背大量的编码最佳实践。
通过将引用的非法赋值推断到更广的范围
范围本质上是生命周期。更清楚一点的是,生命周期' A是一个通用的生命周期参数,可以在编译时基于调用站点的特定范围进行专门化。
是否真的需要显式生命周期来防止[…]错误吗?
一点也不。生命周期是用来防止错误的,但是显式生命周期是用来保护程序员仅有的一点理智。
以下结构中的生命周期注释:
struct Foo<'a> {
x: &'a i32,
}
指定一个Foo实例不应该比它包含的引用活得更久(x字段)。
你在Rust书中看到的例子并没有说明这一点,因为f和y变量同时超出范围。
一个更好的例子是:
fn main() {
let f : Foo;
{
let n = 5; // variable that is invalid outside this block
let y = &n;
f = Foo { x: y };
};
println!("{}", f.x);
}
f确实比f。x指向的变量寿命长。