我正在阅读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后使用(或其他类?)错误?


当前回答

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

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

struct RefPair(&u32, &u32);

这是不同的人生还是相同的人生?从使用的角度来看,结构体RefPair<'a, 'b>(&'a u32, &'b u32)与结构体RefPair<'a>(&'a u32, &'a u32)非常不同。

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

其他回答

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

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

其他答案都有突出的要点(fjh的具体例子中需要显式生命期),但忽略了一个关键问题:为什么编译器会告诉你你弄错了,还需要显式生命期?

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

fn foo() -> _ {  
    ""
}

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

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

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

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

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

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

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

范围本质上是生命周期。更清楚一点的是,生命周期' A是一个通用的生命周期参数,可以在编译时基于调用站点的特定范围进行专门化。

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

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

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

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

struct RefPair(&u32, &u32);

这是不同的人生还是相同的人生?从使用的角度来看,结构体RefPair<'a, 'b>(&'a u32, &'b u32)与结构体RefPair<'a>(&'a u32, &'a u32)非常不同。

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

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

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

Putting an explicit lifetime annotation on a function restricts the type of code that may appear inside that function. Explicit lifetimes allow the compiler to ensure that your program is doing what you intended. If you (the compiler) want(s) to check if a piece of code is valid, you (the compiler) will not have to iteratively look inside every function called. It suffices to have a look at the annotations of functions that are directly called by that piece of code. This makes your program much easier to reason about for you (the compiler), and makes compile times managable.

第一点。,考虑以下用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]])

这种行为总是让我吃惊。实际上,df与ar共享内存,所以当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),编译器会报错生存期不匹配。在正确的地方。在正确的时间。调试是轻而易举的事。