Reference 为什么Rust中需要显式寿命?

Reference 为什么Rust中需要显式寿命?,reference,rust,static-analysis,lifetime,Reference,Rust,Static Analysis,Lifetime,我在读《铁锈书》的第二部分,在一个命名/显式的生命周期中,我遇到了这个例子: struct Foo<'a> { x: &'a i32, } fn main() { let x; // -+ x goes into scope // | { // | let y = &5;

我在读《铁锈书》的第二部分,在一个命名/显式的生命周期中,我遇到了这个例子:

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

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

然而,在类型定义中,显式生存期是不可避免的。例如,此处存在歧义:

struct RefPair(&u32, &u32);

这些生命周期是不同的还是相同的?从使用角度来看,它确实很重要,
struct-RefPair(&'a-u32,&'b-u32)
struct-RefPair有很大的不同,让我们看看下面的示例

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)
    };
}

书中的箱子设计得很简单。生命周期这个话题被认为是复杂的


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

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

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

编译器根本不可能找到答案。

其他答案都有显著的要点(),但缺少一个关键点:为什么编译器会告诉您错误时需要显式的生存期

这实际上与“当编译器可以推断出显式类型时,为什么需要显式类型”相同。一个假设的例子:

fn foo() -> _ {  
    ""
}
当然,编译器可以看到我返回了一个
&'static str
,那么程序员为什么要键入它呢

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

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

编译器还有一个效率优势——只需解析函数签名,即可验证类型和生命周期。更重要的是,它对程序员有效率上的好处。如果我们没有明确的生命周期,这个函数做什么:

fn foo(a: &u8, b: &u8) -> &u8
如果不检查源代码就无法判断,这与大量编码最佳实践背道而驰

通过推断引用非法分配到更大范围

作用域本质上是生命周期。更清楚一点,lifetime
'A
是一个通用的lifetime参数,可以在编译时根据调用站点使用特定的作用域进行专门化

是否确实需要显式生命周期来防止[…]错误


一点也不。需要使用生命周期来防止错误,但需要显式的生命周期来保护几乎没有理智的程序员所拥有的东西。

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

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

现在,
f
确实比
f.x

所指的变量寿命长,我在这里找到了另一个很好的解释:

一般来说,只有在引用正确的情况下才可能返回引用 从过程的参数派生。在这种情况下,指针 结果将始终与其中一个参数具有相同的寿命; 命名的生存期指示所使用的参数


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

同样,如果一个结构包含两个引用(作为两个成员字段),那么该结构的成员函数有时可能返回第一个引用,有时返回第二个引用。再次明确的生命周期防止了这种歧义


在一些简单的情况下,编译器可以推断生存期。

示例不起作用的原因很简单,因为Rust只具有局部生存期和类型推断。你的建议需要全局推断。每当你有一个生命周期无法省略的引用时,就必须对它进行注释。

作为一个生锈的新手,我的理解是,明确的生命周期有两个目的

  • 在函数上放置显式生存期注释会限制该函数中可能出现的代码类型。显式生命周期允许编译器确保您的程序按预期执行

  • 如果您(编译器)想要检查一段代码是否有效,那么您(编译器)不必迭代地查看每个调用的函数。只要看一看由该段代码直接调用的函数的注释就足够了。这使您的程序(编译器)更易于推理,并使编译时间可管理

  • 第1点,考虑Python中编写的以下程序:

    将熊猫作为pd导入
    将numpy作为np导入
    def第二排(ar):
    返回ar[0]
    def工作(秒):
    df=pd.DataFrame(数据=秒)
    df.loc[0,0]=1
    def main():
    # .. 加载数据。。
    ar=np.array([[0,0],[0,0]])
    # .. 在第二排做些工作。。
    第二行=第二行(ar)
    工作(第二)
    # .. 很久以后。。
    打印(报告(ar))
    如果名称=“\uuuuu main\uuuuuuuu”:
    main()
    
    哪个会打印

    数组([[1,0],
    [0, 0]])
    
    这种行为总是让我感到惊讶。发生的情况是,
    df
    ar
    共享内存,因此当
    df
    的某些内容发生变化时
    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);
    }
    
    #[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);
    }