在函数签名中解释将两个引用绑定到具有不同作用域参考的相同生命周期的含义。

10

我一直在努力理解Rust借用和所有权模型。

假设我们有以下代码:

fn main() {
    let a = String::from("short");
    {
        let b = String::from("a long long long string");
        println!("{}", min(&a, &b));
    }
}

fn min<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() < b.len() {
        return a;
    } else {
        return b;
    }
}
min() 函数返回两个 引用 字符串中较短的那个。 main() 函数传入两个字符串引用,它们的定义在不同的作用域内。我使用了 String::from() 以使这些引用没有静态生命周期。该程序正确打印出short这是 Rust Playground 中的示例
如果我们参考Rustonomicon(我很感激它是一个正在进行中的文档),我们可以了解到像下面这样的函数签名的含义:
fn as_str<'a>(data: &'a u32) -> &'a str

意味着该函数:

接受一个的引用,并承诺可以生成一个能够与其相同生命周期的引用。

现在让我们转向我的示例中的min()签名:

fn min<'a>(a: &'a str, b: &'a str) -> &'a str

这更加复杂,因为:

  • 我们有两个输入引用。
  • 它们的指向在不同的作用域中定义,这意味着它们的有效期不同(a有效时间更长)。

使用与上述引用语类似的措辞,min()函数签名的意思是什么?

  1. 该函数接受两个引用并承诺产生一个到str的引用,可以像ab的引用一样长久存在吗?感觉有些不对,如果我们从min()返回b的引用,那么该引用显然不能在main()a的生命周期内有效。

  2. 该函数接受两个引用并承诺产生一个到str的引用,可以像ab中较短寿命的引用一样长久存在吗?可能可行,因为ab的两个引用都在main()的内部作用域中保持有效。

  3. 完全不同的东西?

总之,我不理解将min()的两个输入引用的生命周期绑定到相同的生命周期意味着什么,当它们的指向在调用者的不同作用域中定义。

3个回答

4

它是 (2):返回的引用的生命周期与较短的输入生命周期相同。

然而,从函数的角度来看,两个输入生命周期实际上是相同的(都是 'a)。因此,鉴于变量 a 明显比 b 的寿命更长,这是如何工作的呢?

诀窍在于调用者缩短了其中一个引用的寿命,以匹配 min() 函数签名。如果你有一个引用 &'x T,你可以将其转换为 &'y T,当且仅当 'x'y 寿命更长(也写作:'x: 'y)。这很容易理解(我们可以缩短引用的生命周期而没有坏后果)。编译器会自动执行此转换。所以 想象一下,编译器将你的 main() 转换成:

let a = String::from("short");
{
    let b = String::from("a long long long string");

    // NOTE: this syntax is not valid Rust! 
    let a_ref: &'a_in_main str = &a;
    let b_ref: &'b_in_main str = &b;
    println!("{}", min(&a as &'b_in_main str, &b));
    //                    ^^^^^^^^^^^^^^^^^^
}

这与所谓的“子类型化”有关,您可以在这个优秀答案中了解更多相关信息。 总结一下:调用方缩短一个生命周期以匹配函数签名,使得函数可以假定两个引用具有相同的生命周期。

很棒的回答!谢谢,我会等到今天结束看看是否还有其他人发言。 - Edd Barrett
另外,我在想是否应该尝试提交一个PR到Rustonomicon,添加这个例子。你觉得呢?肯定可以为我节省一些时间。 - Edd Barrett
1
@EddBarrett:我想维护者们会非常高兴得到更多的贡献,特别是来自初学者,因为初学者最适合指出对他们来说是障碍物的内容。您可能需要先开一个问题,讨论您关于开发这个被忽略的主题的想法:这样您就可以在不投入太多时间的情况下了解他们的想法,他们可以在您开始前指导您的工作(也许他们更想要一个更高级的新章节?也许他们更想把它放在这个例子之前但在另一个例子之后?...)。 - Matthieu M.
嗯,我看法正好相反 - 我会添加一个答案。 - Chris Emerson

2

我会去做(3)其他事情

你的函数签名如下:

fn min<'a>(a: &'a str, b: &'a str) -> &'a str { ...}

// ...
min(&a, &b)

'a不是被借用对象的生命周期,而是编译器为此调用生成的新生命周期。在调用期间,ab将被借用(或可能被重新借用),并延长到返回值的作用域(因为它引用了相同的'a)。

以下是一些例子:

let mut a = String::from("short");
{
    let mut b = String::from("a long long long string");
    // a and b borrowed for the duration of the println!()
    println!("{}", min(&a, &b));
    // a and b borrowed for the duration of the expression, but not
    // later (since l is not a reference)
    let l = min(&a, &b).len();

    {
        // borrowed for s's scope
        let s = min(&a, &b);
        // Invalid: b is borrowed until s goes out of scope
        // b += "...";
    }
    b += "...";  // Ok: b is no longer borrowed.
    // Borrow a and b again to print:
    println!("{}", min(&a, &b));
}

你可以看到,任何单独调用的'a 生命周期都不同于实际借用的ab的生命周期,尽管当然两者都必须比每次调用生成的生命周期更长。

(Playground)


1
我对你的回答持保留态度(尽管我已经点赞了,因为借用很重要)。从被调用者的角度来看,我认为借用并不重要。因此,从被调用者的角度来看,答案是(2)=> 被调用者保证结果可以存活在 'a 的生命周期内,即更短的生命周期。无论结果实际上是否能够存活那么长时间...都没有关系。它不能再活得更久了。 - Matthieu M.
@MatthieuM。如果我们从被调用者的角度来看,只有一个生命周期,所以没有太多可说的。(至少从 我的 角度来看!) - Chris Emerson
我想说的是,有两种看待这个问题的方式:(1)结果的最大生命周期是多久?和(2)参数借用多长时间?在我看来,OP正在追求(1),对(2)不太感兴趣。当然,我可能错了,我不是读心术士! - Matthieu M.
好的,我理解为“&a&b如何压缩到一个生命周期'a中。”我也不是心灵读者! - Chris Emerson
这就是为什么我投票支持你和Lukas的答案,你们两个都阐明了完整有效的观点...而我不知道原帖作者想要哪个...也许原帖作者实际上需要两个观点,因为对于初学者来说并不容易立刻看出其中的区别 :) - Matthieu M.

1
除了@Lukas在答案中提到的内容,您还可以将函数的签名阅读为-返回的引用有效,直到传递的引用都有效为止,即参数寿命之间存在合取(也称为AND)。
还有更多要说的。以下是两个代码示例:
    let a = String::from("short");
    {
        let c: &str;
        let b = String::from("a long long long string");
        c = min(&a, &b);

    } 

AND

let a = String::from("short");
    {
        let b = String::from("a long long long string");
        let c: &str;
        c = min(&a, &b);

    }

第一个不起作用(第二个可以)。虽然它们在同一作用域中,看起来bc的生命周期相同,但作用域中的顺序也很重要,因为在第一种情况下,b的生命周期将在c之前结束。

谢谢!实际上,我认为在编译器的眼中,cb处于不同的作用域中,因为每个let绑定都会创建一个新的隐式作用域(根据Rustonomicon)。 - Edd Barrett

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