为什么生命周期名称会出现在函数类型的一部分?

24

我相信,这个函数声明告诉Rust函数的输出生命周期与其s参数的生命周期相同:

fn substr<'a>(s: &'a str, until: u32) -> &'a str;
         ^^^^

我认为编译器只需要知道这个(1):

fn substr(s: &'a str, until: u32) -> &'a str;

函数名后面的注释<'a>是什么意思?编译器为什么需要它,它有什么作用?


(1):我知道由于生命周期省略,它甚至不需要知道更少。但这个问题是关于显式指定生命周期的。

3个回答

38
让我详细说明一下之前的答案...
引用函数名称后面的注解<'a>是什么意思?我不会用“注解”这个词来描述它。与<T>引入泛型类型参数类似,<'a>引入了泛型生命周期参数。在不引入泛型参数的情况下无法使用任何泛型参数,对于泛型函数,这种引入发生在函数名称后面。您可以将泛型函数视为函数族。因此,您可以为每个泛型参数组合获得一个函数。例如,substr::<'x>就是该函数族的一个特定成员,适用于某个生命周期'x
如果您不清楚何时以及为什么必须明确生命周期参数,请继续阅读...
生命周期参数始终与所有引用类型相关联。当您编写:
fn main() {
    let x = 28374;
    let r = &x;
}

编译器知道 x 存在于 main 函数的花括号包围的作用域中。在内部,它使用某个生命周期参数来标识此作用域。对我们来说,它是匿名的。当您取地址 x 时,将获得特定引用类型的值。引用类型是引用类型的二维家族的一种成员。一个轴是引用指向的内容的类型,另一个轴是用于两个约束条件的生命周期:

  1. 引用类型的生命周期参数表示您可以保留该引用的时间上限
  2. 引用类型的生命周期参数表示您可以使引用指向的事物的生命周期下限。

这些约束条件共同在 Rust 的内存安全故事中发挥着至关重要的作用。这里的目标是避免悬空引用。我们希望排除指向某个我们不允许再使用的内存区域的引用,因为它曾经指向的东西已经不存在了。

可能会引起混淆的一个潜在来源可能是生命周期参数大多数时间是不可见的事实。但这并不意味着它们不存在。引用类型始终具有生命周期参数。但是这样的生命周期参数不必具有名称,并且大多数情况下我们不需要提及它,因为编译器可以自动为生命周期参数分配名称。这称为“生命周期省略”。例如,在以下情况下,您不会看到任何生命周期参数被提及:

fn substr(s: &str, until: u32) -> &str {…}

但是这样写也没问题。实际上,这是更显式的简写语法。

fn substr<'a>(s: &'a str, until: u32) -> &'a str {…}

在这里,编译器自动将“输入生命周期”和“输出生命周期”赋予相同的名称,因为这是一个非常常见的模式,也很可能是您想要的。由于这种模式非常普遍,编译器让我们不必对生命周期说任何话。它假定我们基于一些“生命周期省略”规则(至少在此处有文档想要更明确的形式。
有时候,显式生命周期参数是不可选的。例如,如果您写下:
fn min<T: Ord>(x: &T, y: &T) -> &T {
    if x <= y {
        x
    } else {
        y
    }
}

编译器会出现错误,因为它将解释上述声明为:
fn min<'a, 'b, 'c, T: Ord>(x: &'a T, y: &'b T) -> &'c T { … }

因此,对于每个引用,都会引入一个单独的生命周期参数。但是,在此签名中没有关于生命周期参数如何相互关联的信息。这个泛型函数的用户可以使用任何生命周期。这在其主体内是一个问题。我们试图返回xy。但x的类型为&'a T,这与返回类型&'c T不兼容。 y也是同样的道理。由于编译器对这些生命周期关系一无所知,将这些引用作为&'c T类型的引用返回是不安全的。
&'a T类型的值转换为&'c T类型的值是否有可能是安全的?是的。如果生命周期'a等于或大于生命周期'c,则是安全的。或者换句话说,'a: 'c。因此,我们可以这样编写
fn min<'a, 'b, 'c, T: Ord>(x: &'a T, y: &'b T) -> &'c T
      where 'a: 'c, 'b: 'c
{ … }

我们可以轻松地这样写代码,而不需要编译器对函数体进行抱怨。但实际上,这是不必要的复杂操作。我们也可以简单地编写:

fn min<'a, T: Ord>(x: &'a T, y: &'a T) -> &'a T { … }

使用一个生命周期参数来处理所有内容。编译器能够推断出'a作为调用现场参数引用的最小生命周期,因为我们为两个参数都使用了相同的生命周期名称。而这个生命周期恰好是我们需要的返回类型。

希望这回答了你的问题。 :) 祝好!


2
这是一个很棒的答案,比我的回答更深入地解释了生命周期的含义!它还方便地解释了为什么我的“无意义的例子”实际上是无意义的! - Shepmaster
@Shepmaster:谢谢。 :) - sellibitze
fn min<'a, 'b, 'c, T: Ord>(x: &'a T, y: &'b T) -> &'c T where 'a: 'c, 'b: 'cfn min<'a, T: Ord>(x: &'a T, y: &'a T) -> &'a T { ... } 语义上有区别吗?还是它们的行为完全相同? - Léo
1
@Léo:实际上它们只是具有不同数量的生命周期参数而已。它们都接受相同类型的参数。 - sellibitze

17
“在函数名称后面添加 <'a> 注释是什么意思?”
fn substr<'a>(s: &'a str, until: u32) -> &'a str;
//       ^^^^

这里声明了一个通用的生命周期参数。它类似于通用的类型参数(通常表示为<T>),调用函数的人可以决定生命周期是什么。就像你说的,结果的生命周期将与第一个参数的生命周期相同。
所有生命周期名称都是等价的,除了一个:`'static`。此生命周期预设为“保证在整个程序生命周期内都存在”。
最常见的生命周期参数名称可能是`'a`,但您可以使用任何字母或字符串。单个字母最常见,但任何`snake_case`标识符都是可以接受的。
编译器为什么需要它,并且如何处理它?
Rust通常更喜欢事情明确,除非有很好的人体工程学效益。对于生命周期,生命周期省略可以处理大约85%以上的情况,这似乎是一个明显的胜利。

类型参数和其他类型在同一个命名空间中 - T 是泛型类型还是有人给结构体命名了?因此,类型参数需要具有显式注释,以表明 T 是参数而不是真实类型。然而,生命周期参数没有这个问题,所以这不是原因。

相反,明确列出类型参数的主要好处是可以控制 多个 参数之间的交互。以下是一个无意义的例子:

fn better_str<'a, 'b, 'c>(a: &'a str, b: &'b str) -> &'c str
where
    'a: 'c,
    'b: 'c,
{
    if a.len() < b.len() {
        a
    } else {
        b
    }
}

我们有两个字符串,并且输入字符串可能具有不同的生命周期,但必须同时比结果值更长。

另一个例子,正如DK所指出的那样,结构体可以具有自己的生命周期。我也举了一个有点荒谬的例子,但希望能传达这一点:

struct Player<'a> {
    name: &'a str,
}

fn name<'p, 'n>(player: &'p Player<'n>) -> &'n str {
    player.name
}

生命周期可能是 Rust 中比较令人费解的部分之一,但当你开始理解它们时,它们非常棒。


我不理解编译器为什么需要<'a>。我已经编辑了我的问题(希望)解释我为什么感到困惑。 - Wayne Conrad
你做得非常出色。你的无意义示例清楚地表明,使用生命周期参数可以做的事情远不止我简单示例中所展示的。 - Wayne Conrad
2
还要考虑当您在方法impl中使用一个结构体上有一个生命周期参数时会发生什么;否则编译器怎么知道生命周期应该绑定到谁? - DK.
哇,这是我第一次看到“outlives”生命周期参数声明。这很不错。 - Ash Wilson
看起来我们两个同时想到了同一个例子。 :) - sellibitze
哇塞,生命周期现在有意义了。这是最清晰明了的生命周期示例;谢谢你。 - Qix - MONICA WAS MISTREATED

1
"<'a>" 注解只是声明函数中使用的生命周期,就像泛型参数 <T> 一样。
fn subslice<'a, T>(s: &'a [T], until: u32) -> &'a [T] { \\'
    &s[..until as usize]
}

请注意,在您的示例中,所有生命周期都可以被推断出来。
fn subslice<T>(s: &[T], until: u32) -> &[T] {
    &s[..until as usize]
}

fn substr(s: &str, until: u32) -> &str {
    &s[..until as usize]
}

playpen example

可以翻译为:

{{链接1:playpen example}}


我不得不承认,我不理解playpen示例。它使用了生命周期省略,但是我的问题是关于显式生命周期的。 - Wayne Conrad
我觉得我没有看到(1)部分。 - tafia
我编辑了我的答案,添加了(1)部分。这可能是你没有看到它的原因。 - Wayne Conrad

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