【问题标题】:Why are explicit lifetimes needed in Rust?为什么 Rust 需要显式生命周期?
【发布时间】:2015-10-15 01:03:27
【问题描述】:

我正在阅读 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:在内部范围完成后,f 和因此&amp;f.x 变得无效,不应该被分配给x

我的问题是这个问题可以很容易地被分析掉 没有 使用 explicit 'a 生命周期,例如通过推断非法分配对 a 的引用范围更广 (x = &amp;f.x;)。

在哪些情况下实际上需要显式生命周期来防止 use-after-free(或其他一些类?)错误?

【问题讨论】:

标签: reference rust static-analysis lifetime


【解决方案1】:

其他答案都有重点 (fjh's concrete example where an explicit lifetime is needed),但缺少一个关键点:当编译器会告诉你错误时,为什么需要显式生命周期? p>

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

fn foo() -> _ {  
    ""
}

当然,编译器可以看到我返回的是&amp;'static str,那程序员为什么要输入呢?

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

函数是阻止更改代码的影响的自然边界。如果我们允许从代码中完全检查生命周期,那么看似无辜的更改可能会影响生命周期,这可能会导致远处的函数出现错误。这不是一个假设的例子。据我了解,当您依赖顶级函数的类型推断时,Haskell 会遇到这个问题。 Rust 将这个特殊问题扼杀在了萌​​芽状态。

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

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

如果不检查源代码就无法判断,这将违背大量编码最佳实践。

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

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

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

一点也不。 生命周期是防止错误所必需的,但需要明确的生命周期来保护那些头脑清醒的程序员所拥有的东西。

【讨论】:

  • @jco 想象一下,您有一些顶级函数 f x = x + 1 没有您在另一个模块中使用的类型签名。如果您稍后将定义更改为f x = sqrt $ x + 1,其类型将从Num a =&gt; a -&gt; a 更改为Floating a =&gt; a -&gt; a,这将导致所有调用f 的调用站点出现类型错误,例如Int 参数。拥有类型签名可确保在本地发生错误。
  • “范围本质上是生命周期。更清楚一点,生命周期 'a 是一个通用生命周期参数,可以在调用时使用特定范围进行专门化。” 哇这是一个非常好的、有启发性的观点。如果它明确地包含在本书中,我会喜欢它。
  • @fjh 谢谢。只是想看看我是否理解它-关键是如果在添加sqrt $之前明确说明了类型,则更改后只会发生本地错误,而其他地方不会出现很多错误(这要好得多如果我们不想改变实际类型)?
  • @jco 没错。不指定类型意味着您可能会意外更改函数的接口。这就是强烈建议在 Haskell 中注释所有顶级项目的原因之一。
  • 此外,如果一个函数接收到两个引用并返回一个引用,那么它有时可能会返回第一个引用,有时会返回第二个引用。在这种情况下,不可能推断返回的引用的生命周期。显式生命周期有助于避免/澄清这种情况。
【解决方案2】:

让我们看看下面的例子。

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

【讨论】:

  • 编译器不运行该函数,也不知道返回的是哪个(x 或 y),因此编译器无法确定返回值的生命周期。
  • @towry 借用检查器进行基于分支的程序分析,因此它确实知道返回值的生命周期。如果函数签名与返回的生命周期不匹配,它将引发编译错误。
【解决方案3】:

生命周期注解结构如下:

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

【讨论】:

    【解决方案4】:

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

    然而,在类型定义中,显式的生命周期是不可避免的。比如这里有歧义:

    struct RefPair(&u32, &u32);
    

    这些生命周期应该不同还是应该相同?从使用的角度来看,struct RefPair&lt;'a, 'b&gt;(&amp;'a u32, &amp;'b u32)struct RefPair&lt;'a&gt;(&amp;'a u32, &amp;'a u32) 非常不同。

    现在,对于简单的情况,比如你提供的那个,理论上编译器可以像在其他地方一样elide lifetimes,但是这种情况非常有限,不值得在编译器,而这种清晰的增益至少是值得怀疑的。

    【讨论】:

    • 你能解释一下为什么它们有很大不同吗?
    • @A.B.第二个要求两个引用共享相同的生命周期。这意味着 refpair.1 不能比 refpair.2 寿命更长,反之亦然——因此两个 ref 都需要指向同一个所有者的东西。然而,第一个只要求 RefPair 的寿命超过它的两个部分。
    • @AB,它编译是因为两个生命周期是统一的 - 因为本地生命周期小于 'static'static 可以在可以使用本地生命周期的任何地方使用,因此在您的示例中 p将其生命周期参数推断为 y 的本地生命周期。
    • @A.B. RefPair&lt;'a&gt;(&amp;'a u32, &amp;'a u32) 表示 'a 将是两个输入生命周期的交集,即在这种情况下 y 的生命周期。
    • @lllogiq “要求 RefPair 的寿命超过它的两个部分”?我虽然是相反的......如果没有 RefPair,&u32 仍然有意义,而 RefPair 的 refs 死了会很奇怪。
    【解决方案5】:

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

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

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

    【讨论】:

      【解决方案6】:

      书中的案例设计非常简单。生命周期这个话题被认为是复杂的。

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

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

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

      编译器绝对没有办法解决这个问题。

      【讨论】:

      • IINM,推断双参数函数的返回类型的生命周期将等价于停止问题 - IOW,在有限的时间内无法确定。
      • "编译器无法轻易推断出具有多个参数的函数的生命周期。" - 除非第一个参数是 &amp;self&amp;mut self - 否则此引用的生命周期将分配给所有省略的输出生命周期。
      【解决方案7】:

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

      一般来说,只有当它们是 派生自过程的参数。在这种情况下,指针 结果将始终与参数之一具有相同的生命周期; 命名生命周期表明是哪个参数。

      【讨论】:

        【解决方案8】:

        您的示例不起作用的原因仅仅是因为 Rust 只有本地生命周期和类型推断。您的建议需要全局推理。每当您有一个生命周期不能省略的引用时,都必须对其进行注释。

        【讨论】:

          【解决方案9】:

          作为 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 共享内存,所以当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的注解,我们发现输出应该是&amp;mut &amp;'b mut [i32],即输出应该是对生命周期为'b的引用的引用(Array第二行的生命周期) .然而,因为我们要返回第一行(它的生命周期为'a),编译器会抱怨生命周期不匹配。在正确的地方。在正确的时间。调试轻而易举。

          【讨论】:

            【解决方案10】:

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

            【讨论】:

              猜你喜欢
              • 2015-09-14
              • 2023-04-11
              • 1970-01-01
              • 2022-11-21
              • 2013-07-03
              • 2015-08-05
              • 1970-01-01
              • 2017-07-04
              • 2017-05-03
              相关资源
              最近更新 更多