为什么我不能使用临时值调用一个方法?

3
我无法在以下代码中调用 Foo::new(words).split_first()
fn main() {
    let words = "Sometimes think, the greatest sorrow than older";
/*
    let foo = Foo::new(words);
    let first = foo.split_first();
*/

    let first = Foo::new(words).split_first();

    println!("{}", first);
}

struct Foo<'a> {
    part: &'a str,
}

impl<'a> Foo<'a> {

    fn split_first(&'a self) -> &'a str {
        self.part.split(',').next().expect("Could not find a ','")
    }

    fn new(s: &'a str) -> Self {
        Foo { part: s }
    }
}

编译器会给我一个错误消息。
error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:8:17
   |
8  |     let first = Foo::new(words).split_first();
   |                 ^^^^^^^^^^^^^^^              - temporary value is freed at the end of this statement
   |                 |
   |                 creates a temporary which is freed while still in use
9  | 
10 |     println!("{}", first);
   |                    ----- borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

如果我先绑定Foo::new(words)的值,然后调用split_first方法是没有问题的。
这两种调用方法本应该是直观上相同的,但实际上有所不同。

3
编译器直接告诉你出了什么问题:“temporary value is freed at the end of this statement”,“creates a temporary which is freed while still in use”以及“borrow later used here”。我认为这个错误信息很清楚明白。如果不是,请告诉我们你的理解。 - hellow
2
可能是重复的问题:在使用构建器模式时出现“借用值未能长时间存活”的错误。原文链接 - E net4
我认为这个重复不适用,因为如果没有错误的显式生命周期阻止,临时变量可以轻松地被删除。 - Jmb
2个回答

7

简短回答:移除 split_first 函数中 self 参数的 'a 生命周期: fn split_first(&self) -> &'a str (playground)。

详细回答:

当你编写以下代码时:

struct Foo<'a> {
    part: &'a str,
}

impl<'a> Foo<'a> {
    fn new(s: &'a str) -> Self {
        Foo { part: s }
    }
}

您正在告诉编译器,所有的Foo实例都与某个生命周期'a相关联,该生命周期必须等于或短于作为参数传递给Foo::new的字符串的生命周期。该生命周期'a可能与每个Foo实例的生命周期不同。然后,当您编写以下内容时:
let words = "Sometimes think, the greatest sorrow than older";
Foo::new(words)

编译器推断出生命周期'a必须等于或短于words的生命周期。除非有其他限制,否则编译器将使用words的生命周期,它是'static,因此在程序的整个生命周期内都有效。
当您添加split_first的定义时:
fn split_first(&'a self) -> &'a str

你正在添加一个额外的约束条件:你说'a必须等于或短于self的生命周期。因此,编译器将采用words的生命周期和临时Foo实例的生命周期中较短的那个,也就是临时实例的生命周期。@AndersKaseorg's answer解释了为什么这样做不起作用。
通过删除self参数上的'a生命周期,我将'a与临时变量的生命周期分离开来,因此编译器可以再次推断出'awords的生命周期,这足以使程序正常运行。

这是一个很好的观点。我从未检查过split_first实际上是否返回其输入的引用,只是它的类型表明它可以。修复类型是更好的解决方案。 - Anders Kaseorg
假设我按照你在简短回答中建议的进行调整,那么生命周期的重构是否实际上会改变汇编代码,还是我们只是向编译器作出关于某些引用的生命周期的可执行承诺? - Clark McCauley
1
@ClarkMcCauley 生命周期对生成的汇编代码没有影响。它们只是为了让编译器(和开发人员)知道一个函数或接口的行为方式,而不需要查看接口另一侧的代码。 - Jmb

6

Foo::new(words).split_first() 的大致含义是

let tmp = Foo::new(words);
let ret = tmp.split_first();
drop(tmp);
ret

如果Rust允许您这样做,ret中的引用将指向已经丢弃的tmp值。因此,Rust禁止这样做是一件好事。如果您在C++中编写等效的单行代码,您将悄悄地获得未定义的行为。
通过自己编写let绑定,您可以延迟丢弃,直到作用域结束,从而扩展了可以安全使用这些引用的区域。
有关更多详细信息,请参见Rust参考中的临时生命周期
*编辑:如Jmb所指出的,特定示例中的真正问题是类型不允许split_first方法的引用指向临时值。
fn split_first(&'a self) -> &'a str

“不够具体”,更好的解决方案是将类型细化为:

fn split_first<'b>(&'b self) -> &'a str

可以缩写为:

fn split_first(&self) -> &'a str

这表明返回的引用不指向 Foo<'a> (仅指向字符串本身),以此保证所期望的保证。

@AndersKaseorg,我想了解一下你提到的大致解释是否有参考资料或文档可供阅读? - Ömer Erden
@ÖmerErden 当然,我已经添加了一个链接。 - Anders Kaseorg
1
我仍然认为这不正确。Rust确实允许您所说的被禁止的示例。此外,在C++中,由于引用是指向足够长寿的“words”,因此这不应该是未定义行为。 - Peter Hall

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