ChatGPT解决这个技术问题 Extra ChatGPT

为什么 Rust 需要显式生命周期?

我正在阅读 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 因此 {3 } 变为无效,不应分配给 x

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

在哪些情况下实际上需要显式生命周期来防止释放后使用(或其他类?)错误?

对于这个问题的未来读者,请注意它链接到本书的第一版,现在有一个 second edition :)

n
nbro

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

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

fn foo() -> _ {  
    ""
}

当然,编译器可以看到我返回了一个 &'static str,那么程序员为什么要键入它呢?

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

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

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

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

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

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

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

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

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


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

让我们看看下面的例子。

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 借用检查器进行基于分支的程序分析,因此它确实知道返回值的生命周期。如果函数签名与返回的生命周期不匹配,它将引发编译错误。
t
trent

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

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


n
nbro

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

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

struct RefPair(&u32, &u32);

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

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


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

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

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

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


c
corazza

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

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


n
nbro

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

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

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

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

编译器绝对没有办法弄清楚这一点。


IINM,推断两个参数函数的返回类型的生命周期将等效于停止问题 - IOW,在有限的时间内无法确定。
“编译器无法轻松推断具有多个参数的函数的生命周期。” - 除非第一个参数是 &self&mut self - 否则此引用的生命周期将分配给所有省略的输出生命周期。
K
Klas. S

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


J
Jonas Dahlbæk

作为 Rust 的新手,我的理解是显式生命周期有两个目的。

在函数上放置显式生命周期注释会限制可能出现在该函数内的代码类型。显式生命周期允许编译器确保您的程序正在执行您想要的操作。如果您(编译器)想要检查一段代码是否有效,您(编译器)将不必迭代地查看每个调用的函数。看一下由那段代码直接调用的函数的注释就足够了。这使您的程序更容易为您(编译器)推理,并使编译时间易于管理。

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


J
Jorge Gonzalez

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


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅