为什么Rust需要明确的生命周期?

时间:2015-07-24 11:15:06

标签: reference rust static-analysis lifetime

我正在阅读Rust书的lifetimes chapter,我在这个例子中看到了命名/显式生命周期:

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 :在内部作用域完成后,{{ 1}}因此f无效,不应分配给&f.x

我的问题是,在没有使用显式 x生命周期的情况下,可以很容易地分析问题,例如通过推断非法分配引用范围更广('a)。

在哪些情况下实际需要明确的生命周期来防止使用后免费(或其他类?)错误?

10 个答案:

答案 0 :(得分:185)

其他答案都有突出点(fjh's concrete example where an explicit lifetime is needed),但缺少一个关键的事情:当编译器告诉你错误时,为什么需要明确的生命周期

这实际上与&#34相同;为什么在编译器可以推断它们时需要显式类型&#34;。一个假设的例子:

fn foo() -> _ {  
    ""
}

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

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

函数是防火墙更改代码效果的自然边界。如果我们要从代码中完全检查生命周期,那么无辜的变化可能会影响生命周期,这可能会导致远程函数出错。这不是一个假设的例子。据我所知,当您依赖顶级函数的类型推断时,Haskell会遇到这个问题。 Rust扼杀了这个特殊的问题。

编译器还有一个效率优势 - 只需要解析函数签名以验证类型和生命周期。更重要的是,它为程序员带来了效率优势。如果我们没有明确的生命周期,那么这个函数会做什么:

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

如果不检查来源,就无法判断,这会违反大量的编码最佳做法。

  

通过推断非法分配对更广范围的引用

范围基本上是生命周期。更清楚的是,生命周期'a通用生存期参数,可以在编译时根据调用站点专门针对特定范围。

  

实际上是否需要明确的生命周期来防止错误?

完全没有。需要生命周期来防止错误,但需要明确的生命周期来保护小小的理智程序员。

答案 1 :(得分:83)

让我们看看下面的例子。

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

答案 2 :(得分:10)

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

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

答案 3 :(得分:8)

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

然而,在类型定义中,显式生命期是不可避免的。例如,这里有一个含糊不清的地方:

struct RefPair(&u32, &u32);

这些生命周期应该不同,还是应该相同?从使用角度来看,它确实很重要,struct RefPair<'a, 'b>(&'a u32, &'b u32)struct RefPair<'a>(&'a u32, &'a u32)非常不同。

现在,对于简单的情况,就像你提供的那样,编译器可能理论上elide lifetimes就像在其他地方一样,但这种情况非常有限,不值得额外的复杂性在编译器中,这种清晰度的提高至少是值得怀疑的。

答案 4 :(得分:5)

本书的案例设计非常简单。生命周期的主题被认为是复杂的。

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

此外,我自己的optional包有一个OptionBool类型,其as_slice方法的签名实际上是:

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

绝对没有办法让编译器想出那个。

答案 5 :(得分:3)

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

  

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

答案 6 :(得分:3)

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

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

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

答案 7 :(得分:1)

作为Rust的新手,我的理解是明确的生存期有两个目的。

  1. 在函数上放置显式生命周期注释会限制该函数内可能出现的代码类型。显式生存期使编译器可以确保您的程序正在执行您想要的操作。

  2. 如果您(编译器)想要检查一段代码是否有效,则您(编译器)将不必迭代查看每个调用的函数。只需看看由该段代码直接调用的功能的注释即可。这使您的程序(编译器)更容易为您进行推理,并使编译时间可管理。

在第1点上,考虑以下用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共享内存,因此,当dfwork的某些内容发生更改时,该更改也会感染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),因此编译器会抱怨生命周期不匹配。在正确的地方。在正确的时间。调试很容易。

答案 8 :(得分:0)

您的示例不起作用的原因仅仅是因为Rust只有本地生命周期和类型推断。您的建议要求全球推断。每当你有一个不能省略其生命周期的参考时,它必须加注。

答案 9 :(得分:0)

我认为生命周期注释是关于给定引用的协定,仅在接收范围内有效,而在源范围内仍然有效。在同一生存期类型中声明更多引用将合并范围,这意味着所有源引用都必须满足此合同。 这种注释使编译器可以检查合同是否履行。