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

cor*_*zza 183 static-analysis reference lifetime 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
Run Code Online (Sandbox Code Playgroud)

我很清楚,编译器阻止的错误是在内部作用域完成后分配给的引用的释放x后使用,f因此&f.x变为无效,并且不应该被分配给x.

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

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

She*_*ter 192

其他答案都有重点(fjh的具体例子,需要明确的生命周期),但缺少一个关键的事情:为什么在编译器告诉你错误的时候需要明确的生命周期?

这实际上是与"编译器可以推断它们时为什么需要显式类型"相同的问题.一个假设的例子:

fn foo() -> _ {  
    ""
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

fn foo(a: &u8, b: &u8) -> &u8
Run Code Online (Sandbox Code Playgroud)

没有检查源代码就不可能分辨出来,这会违反大量的编码最佳实践.

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

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

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

一点也不.生存期,需要防止错误,但需要明确的寿命,以保护那点理智程序员们.

  • @jco想象一下,你有一些顶级函数`fx = x + 1`没有你在另一个模块中使用的类型签名.如果稍后将定义更改为`fx = sqrt $ x + 1`,则其类型从`Num a => a - > a`变为`Floating a => a - > a`,这将导致类型错误调用`f`的调用站点,例如`Int`参数.具有类型签名可确保在本地发生错误. (17认同)
  • *"范围是生命周期,基本上.更清楚一点,生命周期'a是一个通用的生命周期参数,可以在调用时专门针对特定范围."*哇,这是一个非常好的,有启发性的点.如果它明确包含在书中,我会喜欢它. (9认同)
  • @jco完全正确.不指定类型意味着您可能会意外更改函数的接口.这是强烈鼓励在Haskell中注释所有顶级项目的原因之一. (4认同)
  • 此外,如果函数接收到两个引用并返回引用,那么它有时可能返回第一个引用,有时返回第二个引用.在这种情况下,无法推断返回的引用的生命周期.明确的生命周期有助于避免/澄清这种情况. (4认同)
  • @fjh 谢谢。只是为了看看我是否理解它 - 关键是如果在添加 `sqrt $` 之前明确声明了类型,那么在更改后只会发生局部错误,而在其他地方不会发生很多错误(这是很多如果我们不想更改实际类型会更好)? (2认同)
  • 关于这句话:"*主要原因是,虽然编译器可以看到你的代码做了什么,但它不知道你的意图是什么.*"我想补充一点,以下是另一个"主要原因":它编译器更容易检查一组用户提供的生命周期注释是否正确,因为它独立地导出了使某些代码工作的生命周期.如果代码比问题中的简单示例更复杂,那么在许多重要情况下,后一个问题可能是完全不可能的. (2认同)

fjh*_*fjh 87

我们来看看下面的例子.

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)
    };
}
Run Code Online (Sandbox Code Playgroud)

在这里,显性生命周期很重要.这是因为结果与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
Run Code Online (Sandbox Code Playgroud)

  • @towry Borrow 检查器进行基于分支的程序分析,因此它确实知道返回值的生命周期。如果函数签名与返回的生命周期不匹配,它将引发编译错误。 (4认同)

小智 12

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

struct Foo<'a> {
    x: &'a i32,
}
Run Code Online (Sandbox Code Playgroud)

指定Foo实例不应超过它包含的引用(x字段).

您在书中锈碰到的例子并没有说明这一点,因为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);
}
Run Code Online (Sandbox Code Playgroud)

现在,f真正超过了指向的变量f.x.


Vla*_*eev 9

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

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

struct RefPair(&u32, &u32);
Run Code Online (Sandbox Code Playgroud)

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

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

  • @AB`RefPair <"一>(&"一个U32,&"一U32)`意味着`" A`将是两个输入寿命的交叉点,即,在这种情况下y`的`寿命. (5认同)
  • 你能解释为什么他们与众不同吗? (2认同)
  • @AB 第二个要求两个引用共享相同的生命周期。这意味着 refpair.1 的寿命不能比 refpair.2 长,反之亦然 - 因此两个 refs 需要指向具有相同所有者的东西。然而,第一个仅要求 RefPair 的寿命比其两个部分都长。 (2认同)
  • @AB,它编译因为两个生命周期都是统一的 - 因为本地生命周期小于"静态","静态"可以在任何可以使用本地生命周期的地方使用,因此在你的例子中,`p`将推断其生命周期参数作为`y`的本地生命周期. (2认同)

cor*_*zza 7

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

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


Mic*_*ser 7

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

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

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


llo*_*giq 6

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

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

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

fn as_slice(&self) -> &'static [bool] { ... }
Run Code Online (Sandbox Code Playgroud)

绝对没有办法让编译器知道这一点。