在结构体中定义多个生命周期有什么用处?

71
在Rust中,当我们想让一个结构体包含引用时,通常会这样定义它们的生命周期:

struct Foo<'a> {
    x: &'a i32,
    y: &'a i32,
}

但是在同一个结构体中为不同的引用定义多个生命周期也是可能的:

struct Foo<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

什么情况下这样做有用呢? 能否提供一些示例代码,当两个生命周期为'a时无法编译,但当生命周期为'a'b时可以编译(反之亦然)?

4个回答

42

熬夜过后,我终于想出了一个例子,能够很好地说明生命周期的重要性。以下是代码:

static ZERO: i32 = 0;

struct Foo<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

fn get_x_or_zero_ref<'a, 'b>(x: &'a i32, y: &'b i32) -> &'a i32 {
    if *x > *y {
        return x
    } else {
        return &ZERO
    }
}

fn main() {
    let x = 1;
    let v;
    {
        let y = 2;
        let f = Foo { x: &x, y: &y };
        v = get_x_or_zero_ref(&f.x, &f.y);
    }
    println!("{}", *v);
}

如果您更改 Foo 的定义为:

struct Foo<'a> {
    x: &'a i32,
    y: &'a i32,
}

那么代码将无法编译。

基本上,如果您想在任何需要其参数具有不同生命周期的函数中使用结构体的字段,则结构体的字段也必须具有不同的生命周期。


9
哈哈哈哈!我刚刚也差不多要写同样的内容时,就遭遇了停电大约15分钟。我正准备发布它。唯一我能想到的情况是当你想要获取聚合值并在使用后拆分其部分,同时不失去生命周期信息。考虑构建一组值(可能涉及生命周期),使用它,然后恢复原始值。 - DK.
4
在get_x_or_zero_ref函数中,'b实际上可以省略,因为它已经被默认的生命周期省略规则所隐含。 - bluss
6
说一个函数“需要”其参数具有不同的生命周期是没有意义的。生命周期参数的目的是防止函数或结构将这些参数融合为单个(推断的)生命周期,因此借用检查器可以区分它们。 - trent

19

由于这个问题在搜索结果中仍然很高,并且我感觉我可以更好地解释,因此我希望在这里重新回答我的问题。请考虑以下代码:

Rust Playground

struct Foo<'a> {
    x: &'a i32,
    y: &'a i32,
}

fn main() {
    let x = 1;
    let v;
    {
        let y = 2;
        let f = Foo { x: &x, y: &y };
        v = f.x;
    }
    println!("{}", *v);
}

还有错误:

error[E0597]: `y` does not live long enough
--> src/main.rs:11:33
|
11 |         let f = Foo { x: &x, y: &y };
|                                 ^^ borrowed value does not live long enough
12 |         v = f.x;
13 |     }
|     - `y` dropped here while still borrowed
14 |     println!("{}", *v);
|                    -- borrow later used here

这里发生了什么?

  1. f.x的生命周期要求至少大到足以囊括x的作用域直至println!语句(因为它初始化为&x,然后被赋值给v)。
  2. Foo的定义指定f.xf.y都使用相同的泛型生命周期'a,因此f.y的生命周期必须至少与f.x一样长。
  3. 但是,这行不通,因为我们将&y分配给了f.y,而yprintln!之前就超出了作用域。错误!

解决方法是允许Foo使用不同的生命周期来处理f.xf.y,我们使用多个泛型生命周期参数来实现:

Rust Playground

struct Foo<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}
现在 f.xf.y 的生命周期不再绑定在一起。 编译器仍将使用一个有效期至 println! 语句的生命周期来处理 f.x。但是,f.y 不再需要使用相同的生命周期,因此编译器可以选择更小的生命周期用于 f.y,例如仅对 y 的作用域有效的生命周期。

13

这是另一个简单的例子,其中结构体定义必须使用两个生命周期才能按预期运行。它不会将聚合分成具有不同生命周期的字段,而是将结构体嵌套到另一个结构体中。

struct X<'a>(&'a i32);

struct Y<'a, 'b>(&'a X<'b>);

fn main() {
    let z = 100;
    //taking the inner field out of a temporary
    let z1 = ((Y(&X(&z))).0).0;  
    assert!(*z1 == z);
}

结构体 Y 有两个生命周期参数,一个用于其包含的字段 &X,另一个用于 X 的包含字段 &z

在操作 ((Y(&X(&z))).0).0 中,X(&z) 被创建为临时变量并被借用。它的生命周期仅限于此操作的范围,在语句结束时失效。但由于 X(&z) 的生命周期与其包含的字段 &z 不同,因此该操作可以返回 &z,其值可以在函数中稍后访问。

如果对于 Y 结构体使用单一生命周期,则此操作将无法正常工作,因为 &z 的生命周期和其包含的结构体 X(&z) 相同,在语句结束时失效;因此返回的 &z 不再有效,无法后续访问。

请参见playground中的代码。


如果将表达式X(&z)提升为自己的变量,即let x = X(&z),则可以删除对Y的额外生命周期。是否有其他方法来强制需要额外的生命周期参数?我目前正在尝试理解为什么函数可能需要>1个生命周期参数。 - Steven Shaw
@StevenShaw 是的。一个单独的变量x将把X(&z)提升到与z相同的作用域级别,而不是在z的构造函数内部临时使用。另一方面,我回答中的情况并不是概念游戏,而是发生在我的实际项目中。我只是将其简化为给定的代码。对于函数来说,拥有多个生命周期参数甚至更为常见。例如,您有两个输入借用,但返回值的生命周期仅依赖于其中一个输入的生命周期。 - Xiao-Feng Li
谢谢,我想可能只有在更广泛的上下文中才能看到它。我已经努力想出一个需要在函数上具有多个生命周期参数的小例子。例如,接受的答案可以简单地删除函数的第二个参数。如果您还删除了main中不必要的作用域,甚至可以删除结构体的第二个参数。https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f8c63e5f51df08323749230931883bf2 我已经记下了您美好的短语“概念游戏”,并将您的书添加到我的愿望清单中。 - Steven Shaw
@StevenShaw 终身省略在编译器无法推断其生命周期时不起作用。一个简单的例子是让函数返回一个具有两个不同生命周期的结构体,例如被接受答案中的 Foo。请参见 https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=31247245d3472547571d99dd02103737。 - Xiao-Feng Li
谢谢你的帮助和毅力!因此,函数需要多个生命周期,而结构需要多个生命周期。这让我们回到了这个非常重要的问题!然而,我发现这里的答案相当牵强,因为我可以轻松地从结构中删除生命周期。例如,我将您最新的 playgroud 更改为具有单个生命周期参数(以及函数)。 https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=237c2fecbde105bd65ec563b2b3beee7 。同样,我想象在一个更大的项目中,必须在结构上使用多个生命周期。 - Steven Shaw
显示剩余2条评论

2

多个生命周期参数,因为输入是嵌套的生命周期参数

struct Container<'a> {
    data: &'a str,
}

struct Processor<'a, 'b> {
    container: &'a Container<'b>,
}

fn process<'a, 'b>(processor: &'a Processor<'a, 'b>) -> &'b str
where
    'a: 'b,
{
    processor.container.data
}

fn main() {
    let data = "Hello, world!";
    let container = Container { data: &data };
    let processor = Processor { container: &container };
    let result = process(&processor);
    println!("Result: {}", result);
}

返回具有较短生命周期的类型

我认为值得一提的是,在Rust中,生命周期参数与其定义的作用域密切相关。通过分配适当的生命周期参数并添加约束,我们确保引用具有有效的生命周期,并避免悬空引用或引用超出其预期作用域的问题。

每个项目或引用都有自己的生命周期,确定其有效和可使用的时间长度。当我们为不同的项目指定不同的生命周期参数时,我们为每个项目赋予一个代表引用有效期的"票"。

考虑以下代码结构:

{
    // Scope of item A
    {
        // Scope of item B
    }
}

Item B只能存在于其范围内,而Item A可以存在于Item A和Item B的范围内。
在上述代码中,函数get_x_or_zero_ref有两个具有不同生命周期的输入引用:'a代表x,'b代表y。通过为它们各自指定一个生命周期参数,我们实际上为它们提供了自己的“票”,以指定引用有效的时间长度,以避免悬空引用。
现在,当我们将get_x_or_zero_ref函数的返回类型指定为'a i32时,我们是在说返回类型的生命周期参数应该与第一个输入引用的生命周期参数相同,即'a。这确保返回的引用不会超出x的作用域。
但是如果我们想将返回类型的生命周期参数指定为'b i32'或'-> &'b i32'或具有较短生命周期的话呢?
在这种情况下,我们试图将'b的生命周期参数分配给返回类型,该返回类型对应于项目B的范围。然而,如果我们不正确处理,可能会出现潜在问题。项目B的生命周期将在其范围内结束,但是默认情况下,项目A的生命周期比项目B的生命周期更长。
这是有问题的,因为我们指定了返回类型具有生命周期'b,该生命周期对应于项目B的范围。然而,由'a表示的项目A的生命周期超过了返回类型的需求。实质上,我们给予返回类型一个有效期比项目A的实际生命周期更短的票。
为了解决这个问题,我们需要对项目A的'a生命周期添加约束,表明与'a相关联的票只在项目B的范围内有效。通过添加这个约束,我们确保返回的引用不会超出其预期的范围,并保持生命周期之间的正确关系。
所以,如果我们想要返回一个寿命较短的项目,我们需要确定是否有另一个寿命更长的输入。然后,我们对寿命更长的输入添加约束,使其票据仅在较短的寿命内有效,并且不会超过其他寿命。
以下是如果我们想要编写具有较短寿命的返回类型的代码。
static ZERO: i32 = 0;

struct Foo<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

// returning lifetime 'b
fn get_x_or_zero_ref<'a, 'b>(x: &'a i32, y: &'b i32) -> &'b i32
where
    // Adding constrain so a will not outlive b
    'a: 'b,
{
    if *x > *y {
        x
    } else {
        &ZERO
    }
}

fn main() {
    let x = 1;
    let v;
    {
        let y = 2;
        let f = Foo { x: &x, y: &y };
        v = get_x_or_zero_ref(&f.x, &f.y);
        println!("{}", *v);
    }
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接