生命周期参数的语义学

9
考虑来自 《Rust编程之书》 的以下示例:
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

据说(重点在于):函数签名现在告诉Rust,在某些生命周期'a中,该函数需要两个参数,都是字符串切片,并且它们的生命周期至少与生命周期'a一样长。此外,函数签名还告诉Rust,从函数返回的字符串切片的生命周期至少与生命周期'a一样长。 实际上,这意味着longest函数返回的引用的生命周期与传入的引用的生命周期中较小的那个的生命周期相同。这些限制正是我们希望Rust强制执行的内容。
粗体句子难道不应该是这样吗:The function signature also tells Rust that the string slice returned from the function will live at most as long as lifetime 'a.?这样,只要xy都存在,返回值就仍然有效,因为后者引用前者。
换句话说,如果xy和返回值都至少与生命周期'a'一样长,那么编译器可以简单地让'a'成为一个空范围(任何项都可以超越),以满足限制条件,这使得注释毫无意义。这是没有意义的,对吧?

2
@Netwave 但如果它存在的时间太长,例如超过了 xy 的寿命,则不会有效。 - nalzok
1
因此,只要 xy 仍然有效,调用者就可以放心地使用返回的值。 - Jmb
1
@Jmb 是的,我正试图理解注释如何将返回值的生命周期与参数的生命周期联系起来。我的意思是,a < ba < c 并不能告诉我们关于 bc 之间关系的任何信息,因此奇怪的是,生命周期注释可以从这个事实中建立链接,即参数和返回值都可以超过某个任意生命周期。 - nalzok
1
用正式的语言表达,该注释翻译为:“对于所有的'a',如果'a≤'x'和'a≤'y',则必须有'a≤'r'(其中''x'',''y''和''r''分别是'x'、'y'和返回值的寿命)。如果要使该关系对于所有''a'都成立,则必须具有''x≤'r'或''y≤'r'。 - Jmb
1
@Jmb 这是一个非常好的解释。我喜欢它!不过还有一个问题:根据书上的说法,请注意,longest函数不需要知道xy的确切寿命,只需要知道可以替换为“'a”的某个作用域,以满足此签名。你认为这里的“某个作用域”应该是“所有作用域”吗? - nalzok
显示剩余6条评论
3个回答

8
用正式的语言表达,这个注释的翻译如下:
对于所有的'a,如果'a≤'x并且'a≤'y,则'a≤'r。
其中,'x'y'r 分别表示 xy 和返回值的生命周期。
这将返回值的生命周期与参数的生命周期联系起来,因为对于该关系要成立的情况下,必须有 'x≤'r 或者 'y≤'r
编译器会在两个时刻使用该注释:
1. 编译带注释的函数时,编译器不知道 xy 的实际生命周期,也不知道 'a(因为像所有泛型参数一样,'a 将在调用点被选择)。但它知道当函数被调用时,调用方将使用与输入约束 'a≤'x'a≤'y 匹配的某个生命周期 'a,并检查函数代码是否遵守输出约束 'a≤'r
2. 调用带注释的函数时,编译器将向其约束求解器添加一个未知范围 'a 以便访问返回值,以及约束条件 'a≤'x'a≤'y 以及由周围代码和特别是 xy 来源以及如何使用返回值所需的任何额外约束。如果编译器能够找到符合所有约束条件的一些范围 'a,则使用该范围编译代码。否则,将出现 "does not live long enough" 错误导致编译失败。

我认为这是颠倒的。应该是对于所有'a,'a <= 'r意味着'a <= 'x和'a <= 'y。 使用对于所有'a,'a<= 'x和'a<= 'y意味着'a <= 'r,'r至少与'x和'y的交集一样长,但可能更长。 - typetetris
在问题中使用的 longest 函数中,如果返回值为 'x',则 'r=='x',如果返回值为 'y',则 'r==y,因此 'r 可能比 'x'y 的交集更长。 - Jmb
仔细重新阅读您的答案后,我发现第二点是我所期望的。对于第一点,函数的代码如何不遵守 'a<='r - typetetris
fn foo<'a, 'b> (x: &'a str, y: &'b str) -> &'a str { return y; } 不能保证 'a <= 'r(因此无法编译)。 - Jmb
谢谢。我感到有点尴尬,没看到那个例子。 - typetetris
显示剩余2条评论

1
我们可以考虑对您示例代码进行轻微的范围修改。
fn main() {
    let string1 = String::from("abcd");

    {
        let string2 = "xyz";
        let result = longest(string1.as_str(), string2);
        println!("The longest string is {}", result);
    }
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

在上面的函数调用longest中,我们认识到变量a的生命周期最终是string2的生命周期,因为参数xy的生命周期必须至少与a一样长,所以如果astring1的生命周期,则传递给longest的第二个参数string2将不能比string1的生命周期更长,而且语句“两个参数的生命周期必须至少与a一样长”将不成立。
我们承认生命周期astring2的生命周期。我们知道由longest返回的字符串切片可以是string1string2。由于我们在声明中约束返回值的生命周期至少与生命周期a一样长,这实际上意味着返回值的生命周期至少与两个生命周期中较短的那个string2的生命周期一样长。
如果longest返回string2,那么返回的字符串切片将与生命周期a完全相同。然而,如果longest返回string1,则返回的字符串切片将与string1的生命周期一样长,这比astring2的生命周期)更长,因此我们说从函数返回的字符串切片至少会与a一样长。
需要注意的重要事项是,我们不知道longest将返回哪个切片,因此我们只允许返回引用的生命周期为两个生命周期中较小的那个,因为在较小的生命周期期间,两个字符串肯定仍然存在。

1

当我第一次阅读书中的那一部分时,我的反应和你一样。现在,我终于在阅读《Rustonomicon》之后理解了这些书的含义。

Rust中的每个引用都有一个生命周期。生命周期是代码区域/跨度,在该区域/跨度内引用正在借用值。显然,这意味着该值将“至少”在我们借用该值的持续时间内存活。

当我们有一个函数定义:
fn longest(x: &str, y: &str) -> &str

此函数返回一个引用给调用者。为了让调用者正确使用返回的引用,调用者需要知道此引用中的值的寿命。 被调用方需要保证引用内部的值至少存活在某个使用该引用的代码跨度内。在此示例中,我使用了lifetime1和lifetime2。我注释了生命周期(这不是有效的Rust代码)以进行可视化。

fn longest<'a, 'b, 'c>(x: &'a str, y: &'b str) -> &'c str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
fn main() {
    'lifetime1: {
        let string1 = String::from("abcd");
        let string2 = "xyz";

        'lifetime2: {
            let result: &'lifetime2 = longest<'lifetime1, 'lifetime1, 'lifetime2>(string1.as_str(), string2);
            println!("The longest string is {}", result);
        }
    }
}

在上面的代码中,调用者保证x中的值至少会存活在'a(即lifetime1)的时间内,y中的值将存活在'b(即lifetime1)的时间内。而被调用者将保证返回引用中的值至少会存活在'c(即lifetime2)的时间内。
因此,“至少”有不同的保证人,对于输入,调用者必须保证它,对于输出,被调用者必须保证它。
现在上面的代码仍然是错误的。函数longest可以从任何地方调用,因此函数体并不确切知道'a、'b和'c之间的关系。我们不能将较小或不相交的生命周期分配给较大的生命周期。
因此,这是一种可能的修复方法:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
fn main() {
    'lifetime1: {
        let string1 = String::from("abcd");
        let string2 = "xyz";

        'lifetime2: {
            let result: &'lifetime2 = longest<'lifetime2>(string1.as_str(), string2);
            println!("The longest string is {}", result);
        }
    }
}

因为string1和string2的生命周期长于lifetime2,我们可以借用一个具有lifetime2的引用。所以上述代码是有效的。
另一种解决方案是在我们最初的解决方案中声明'a和'b比'c大或超集合'c
'a:'c和'b:'c
fn longest<'a: 'c, 'b: 'c, 'c>(x: &'a str, y: &'b str) -> &'c str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
fn main() {
    'lifetime1: {
        let string1 = String::from("abcd");
        let string2 = "xyz";

        'lifetime2: {
            let result: &'lifetime2 = longest<'lifetime1, 'lifetime1, 'lifetime2>(string1.as_str(), string2);
            println!("The longest string is {}", result);
        }
    }
}

现在我们可以将&'a str分配给&'c str,因为'c是'a的子集。

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