为什么在Rust中需要显式的生存期?


199

我正在阅读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:在完成内部作用域之后,f因此&f.x变为无效,并且不应将其分配给x

我的问题是,无需使用显式的 'a生存期,就可以轻松解决问题,例如,通过推断对更大范围的引用的非法分配(x = &f.x;)。

在哪些情况下,真正需要显式生存期来防止“用后使用”(或其他某些类?)错误?



2
对于这个问题的未来读者,请注意它链接到本书的第一版,现在有第二版:)
carols10cents 17/11/19

Answers:


205

其他答案都具有显着性(fjh是需要显式生存期的具体示例),但是却缺少一件事:当编译器告诉您错误为什么需要显式生存期?

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

fn foo() -> _ {  
    ""
}

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

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

功能是防火墙对代码更改效果的自然界线。如果我们允许从代码中完全检查生命周期,那么看起来无害的更改可能会影响生命周期,从而可能导致相距遥远的函数出错。这不是一个假设的例子。据我了解,当您依赖类型推断来实现顶级功能时,Haskell会遇到此问题。鲁斯特消除了这个特殊的问题。

编译器还有一个效率上的好处-只需解析函数签名即可验证类型和生存期。更重要的是,它对程序员有效率的好处。如果我们没有明确的生存期,则此函数的作用是:

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

如果不检查源代码就无法分辨,这会违背大量的编码最佳实践。

通过推断非法转让对更广泛范围的引用

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

真正需要显式的生命周期来防止错误吗?

一点也不。需要生命周期来防止错误,但是需要明确的生命周期来保护几乎没有理智的程序员。


18
@jco假设您有一些顶级函数,f x = x + 1而您没有在另一个模块中使用类型签名。如果以后将定义更改为f x = sqrt $ x + 1,则其类型从更改Num a => a -> aFloating a => a -> a,这将在f使用Int参数调用的所有调用站点上导致类型错误。具有类型签名可确保错误在本地发生。
fjh

11
“作用域本质上是生命周期。更清楚地说,生命周期'a是通用生命周期参数,可以在调用时针对特定作用域进行专门化。”哇,这确实是一个很好的启发性点。如果它明确地包含在本书中,我会很喜欢。
corazza 2015年

2
@fjh谢谢。只是看看我是否理解过-关键是,如果在添加之前明确声明了类型sqrt $,则更改后只会发生局部错误,而在其他地方则不会发生很多错误(如果我们没有这样做,那就更好了)不想更改实际类型)?
corazza 2015年

5
@jco确实如此。不指定类型意味着您可能会意外更改函数的接口。这是强烈建议在Haskell中注释所有顶级项目的原因之一。
fjh 2015年

5
同样,如果一个函数收到两个引用并返回一个引用,则它有时可能返回第一个引用,有时返回第二个引用。在这种情况下,不可能为返回的参考推断寿命。明确的生命周期有助于避免/弄清这种情况。
MichaelMoser

93

让我们看下面的例子。

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

16

生命周期批注采用以下结构:

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


9

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

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

struct RefPair(&u32, &u32);

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

现在,对于简单的情况(如您提供的情况),编译器理论上可以像在其他地方一样延长生存期,但是这种情况非常有限,不值得在编译器中增加额外的复杂性,而这种清晰度的提高将在于至少值得怀疑。


2
您能解释一下为什么他们与众不同吗?
AB

@AB第二个要求两个引用共享相同的生存期。这意味着refpair.1的寿命不能超过refpair.2的寿命,反之亦然–因此,两个裁判都必须指向具有相同所有者的事物。但是,第一个仅要求RefPair的寿命更长。
llogiq

2
@AB,因为两个生存期是统一的,所以进行编译-因为本地生存期较小'static,因此'static可以在可以使用本地生存期的任何地方使用,因此在您的示例p中,其生存期参数将推断为y
弗拉基米尔·马特维夫

5
@AB RefPair<'a>(&'a u32, &'a u32)表示这'a将是两个输入寿命的交集,即在这种情况下为的寿命y
fjh

1
@llogiq“要求RefPair超过其两个部分的寿命”吗?我虽然是相反的话...&u32在没有RefPair的情况下仍然有意义,而refPair的ref死掉将是很奇怪的。
2016年

6

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

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

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

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

编译器绝对不可能找出其中的一个。


IINM推断两个参数函数的返回类型的生存期将等同于暂停问题-IOW,在有限的时间内无法确定。
dstromberg '18


4

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

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

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


1

您的示例不起作用的原因仅是因为Rust仅具有本地生存期和类型推断。您的建议需要全局推断。只要您有一个其生存期不容忽视的参考,就必须对其进行注释。


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]])

这种行为总是让我感到惊讶。发生的事情是与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)的引用的引用。但是,由于我们返回的是第一行(具有lifetime 'a),因此编译器抱怨生命周期不匹配。在正确的地方。在正确的时间。调试很容易。


0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.