为什么Rust中需要显式生命期?

我正在阅读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生命期,问题可以很容易地分析掉没有,例如,通过推断对更广泛的范围(x = &f.x;)的引用的非法赋值。

在哪些情况下,实际上需要显式生命期来防止free后使用(或其他类?)错误?

30094 次浏览

注意,除了结构定义之外,这段代码中没有显式的生存期。编译器完全能够推断main()中的生存期。

然而,在类型定义中,显式生存期是不可避免的。例如,这里有一个歧义:

struct RefPair(&u32, &u32);

这是不同的人生还是相同的人生?从使用的角度来看,这确实很重要,struct RefPair<'a, 'b>(&'a u32, &'b u32)struct RefPair<'a>(&'a u32, &'a u32)非常不同。

现在,对于简单的情况,就像你提供的那样,编译器可以理论上省略一生就像它在其他地方做的那样,但这样的情况是非常有限的,不值得在编译器中增加额外的复杂性,而且这种清晰度的增加至少是有问题的。

让我们来看看下面的例子。

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

书中的箱子设计非常简单。生命的话题被认为是复杂的。

编译器无法轻易推断具有多个参数的函数的生存期。

另外,我自己的可选板条箱有一个OptionBool类型和一个as_slice方法,其签名实际上是:

fn as_slice(&self) -> &'static [bool] { ... }

编译器绝对不可能发现这个。

其他答案都有突出的点(Fjh的具体示例,其中需要显式的生存期),但缺少一个关键的东西:为什么在编译器会告诉你你弄错了时需要显式的生存期?

这实际上和“编译器可以推断显式类型,为什么还需要显式类型”是同一个问题。一个假设的例子:

fn foo() -> _ {
""
}

当然,编译器可以看到我返回了一个&'static str,那么为什么程序员必须输入它呢?

主要原因是,虽然编译器可以看到你的代码做了什么,但它不知道你的意图是什么。

函数是防止代码更改影响的天然边界。如果我们允许从代码中完全检查生存期,那么一个看似无害的更改可能会影响生存期,这可能会在遥远的函数中导致错误。这不是一个假设的例子。据我所知,当对顶级函数依赖类型推断时,Haskell就会遇到这个问题。Rust将这个问题扼杀在萌芽状态。

对于编译器来说,还有一个效率上的好处——只需要解析函数签名来验证类型和生存期。更重要的是,它可以提高程序员的效率。如果我们没有显式的生存期,这个函数会做什么:

fn foo(a: &u8, b: &u8) -> &u8

不检查源代码是不可能知道的,这将违背大量的编码最佳实践。

通过将引用的非法赋值推断到更广的范围

作用域生命周期。更清楚一点的是,生命周期'a是一个通用寿命参数,它可以在编译时根据调用地点指定特定的作用域。

是否真的需要显式生命周期来防止[…]错误吗?

一点也不。一生中是用来防止错误的,但是显式的生命周期是用来保护程序员仅有的一点理智。

以下结构中的生命周期注释:

struct Foo<'a> {
x: &'a i32,
}

指定Foo实例不应该比它包含的引用活得更久(x字段)。

你在Rust书中看到的例子并没有说明这一点,因为fy变量同时超出了作用域。

一个更好的例子是:

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所指向的变量更长寿。

我在这里找到了另一个很好的解释:http://doc.rust-lang.org/0.12.0/guide-lifetimes.html#returning-references

一般来说,只有当引用为时才有可能返回 从过程的参数派生。在这种情况下,是指针 Result将始终具有与其中一个参数相同的生命周期; 命名的生命周期指示是哪个参数

如果一个函数接收两个引用作为参数并返回一个引用,那么该函数的实现有时可能返回第一个引用,有时返回第二个引用。对于给定的调用,无法预测将返回哪个引用。在这种情况下,不可能推断返回引用的生存期,因为每个参数引用可能引用具有不同生存期的不同变量绑定。显式生命周期有助于避免或澄清这种情况。

同样地,如果一个结构体包含两个引用(作为两个成员字段),那么该结构体的成员函数可能有时返回第一个引用,有时返回第二个引用。同样,显式的生存期可以防止这种歧义。

在一些简单的情况下,存在< em >一生省略< / em >,编译器可以推断生存期。

你的例子不能工作的原因很简单,因为Rust只有局部生存期和类型推断。你的建议需要全局推理。当您有一个生命周期不能被省略的引用时,必须对其进行注释。

作为Rust的新手,我的理解是显式生命期有两个目的。

  1. 在函数上使用显式生命周期注释会限制可能出现在该函数中的代码类型。显式的生存期允许编译器确保你的程序正在做你想做的事情。

  2. 如果你(编译器)想要检查一段代码是否有效,你(编译器)不必迭代地查看每个被调用的函数。看一看由这段代码直接调用的函数的注释就足够了。这使得您(编译器)更容易对程序进行推理,并使编译时间易于管理。

第一点。,考虑以下用Python编写的程序:

import pandas as pd
import numpy as np


def second_row(ar):
return ar[0]


def work(second):
df = pd.DataFrame(data=second)
df.loc[0, 0] = 1


def main():
# .. load data ..
ar = np.array([[0, 0], [0, 0]])


# .. do some work on second row ..
second = second_row(ar)
work(second)


# .. much later ..
print(repr(ar))


if __name__=="__main__":
main()

打印出来

array([[1, 0],
[0, 0]])

这种行为总是让我吃惊。正在发生的事情是dfar共享内存,所以当df的一些内容在work中发生变化时,这些变化也会感染ar。然而,在某些情况下,出于内存效率的原因(无复制),这可能正是您想要的。这段代码中的真正问题是second_row函数返回的是第一行而不是第二行;祝你调试好运。

考虑一个用Rust编写的类似程序:

#[derive(Debug)]
struct Array<'a, 'b>(&'a mut [i32], &'b mut [i32]);


impl<'a, 'b> Array<'a, 'b> {
fn second_row(&mut self) -> &mut &'b mut [i32] {
&mut self.0
}
}


fn work(second: &mut [i32]) {
second[0] = 1;
}


fn main() {
// .. load data ..
let ar1 = &mut [0, 0][..];
let ar2 = &mut [0, 0][..];
let mut ar = Array(ar1, ar2);


// .. do some work on second row ..
{
let second = ar.second_row();
work(second);
}


// .. much later ..
println!("{:?}", ar);
}

编译这个,你得到

error[E0308]: mismatched types
--> src/main.rs:6:13
|
6 |             &mut self.0
|             ^^^^^^^^^^^ lifetime mismatch
|
= note: expected type `&mut &'b mut [i32]`
found type `&mut &'a mut [i32]`
note: the lifetime 'b as defined on the impl at 4:5...
--> src/main.rs:4:5
|
4 |     impl<'a, 'b> Array<'a, 'b> {
|     ^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...does not necessarily outlive the lifetime 'a as defined on the impl at 4:5
--> src/main.rs:4:5
|
4 |     impl<'a, 'b> Array<'a, 'b> {
|     ^^^^^^^^^^^^^^^^^^^^^^^^^^

事实上你会得到两个错误,还有一个是'a'b的角色互换了。查看second_row的注释,我们发现输出应该是&mut &'b mut [i32],也就是说,输出应该是一个引用,其生命期为'b (Array的第二行生命期)。然而,因为我们返回的是第一行(它的生存期为'a),编译器会报错生存期不匹配。在正确的地方。在正确的时间。调试是轻而易举的事。

我认为生命周期注释是关于给定ref的契约,它只在接收范围内有效,而在源范围内仍然有效。在同一个生命周期中声明更多的引用会合并作用域,这意味着所有的源引用都必须满足这个契约。 这样的注释允许编译器检查契约是否实现