限制借用 impl Trait 对象的生命周期

Constraining lifetimes of a borrow of an impl Trait object

考虑以下代码:

trait MyTrait<'a, 'b: 'a> {
    fn f(&'b mut self) -> &'a str;
}

struct MyStruct {
    my_string: String,
}

impl<'a, 'b: 'a> MyTrait<'a, 'b> for MyStruct {
    fn f(&'b mut self) -> &'a str {
        &self.my_string
    }
}

fn foo<'a, 'b: 'a>(s: &'b mut impl MyTrait<'a, 'b>) {
    bar(s);
    bar(s); // ERROR
}

fn foo2<'a>(s: &mut MyStruct) {
    bar(s);
    bar(s);
}

fn bar<'a, 'b: 'a>(s: &'b mut impl MyTrait<'a, 'b>) {
    let x = s.f();
}

特征 MyTrait 定义了一个 returns 一个 str 引用的函数。它的生命周期可以解释为 “返回 str 引用的对象将至少与该引用一样长。”.

bar 是一个函数,它接受对实现 MyTrait 并调用 f 的结构的可变引用。函数 foofoo2 相同,唯一的区别是 foo2 接受特定类型,而 foo 接受实现 MyTrait 的任何具体类型。

由于 foo2 可以编译,我希望 foo 也可以编译,但没有。这是错误消息:

error[E0499]: cannot borrow `*s` as mutable more than once at a time
  --> src\bin\main2.rs:17:6
   |
15 | fn foo<'a, 'b: 'a>(s: &'b mut impl MyTrait<'a, 'b>) {
   |            -- lifetime `'b` defined here
16 |     bar(s);
   |     ------
   |     |   |
   |     |   first mutable borrow occurs here
   |     argument requires that `*s` is borrowed for `'b`
17 |     bar(s); // ERROR
   |         ^ second mutable borrow occurs here

据我了解,这是正在发生的事情:MyTrait 强制实现它的对象在生命周期 'b 中存在。对 bar 的第一次调用在整个生命周期 'b 中借用 s,这意味着借用不会像常规借用那样在 bar 结束时丢弃,而只会丢弃在 'b 结束时。 foobar 具有相同的函数签名,因此指定的生命周期将相同。这意味着当 bar 可变地借用 s 时,这个借用只会在 'b 结束时超出范围 - 在 foo 结束时。第二个 bar 调用将遵循相同的逻辑,但是由于已经存在一个可变借用,编译器将不允许创建另一个。

最初我认为一个解决方案是将 bar 的签名重写为:

fn bar<'a, 'b: 'a, 'c: 'b>(s: &'b mut impl MyTrait<'a, 'c>)

希望编译器将其解释为:'b 仅在函数的持续时间内存在,'a 甚至受到进一步限制,对象本身 *s 的生命周期 'c 超过函数 的生命周期。 但是它导致了错误:

error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting requirements

是否可以限制 bar 中的借用仅在 bar 期间有效?

我不确定你是怎么得到你发布的代码的,但我发现最好的经验法则是从没有明确的生命周期参数开始,并且只在编译器说需要它们时才添加它们。

在你的情况下,编译器不需要生命周期参数,很高兴自己把它们全部弄清楚:

trait MyTrait {
    fn f(&mut self) -> &str;
}

struct MyStruct {
    my_string: String,
}

impl MyTrait for MyStruct {
    fn f(&mut self) -> &str {
        &self.my_string
    }
}

fn foo(s: &mut impl MyTrait) {
    bar(s);
    bar(s);
}

fn foo2(s: &mut MyStruct) {
    bar(s);
    bar(s);
}

fn bar(s: &mut impl MyTrait) {
    let x = s.f();
}

特别是,对于任何接受一个引用并返回一个引用的函数,编译器可以计算出返回的引用必须来自参数,因此不需要明确的界限.它实际上在做什么(AFAIK)基本上是插入这些生命周期:

trait MyTrait {
    fn f<'a>(&'a mut self) -> &'a str;
}

impl MyTrait for MyStruct {
    fn f<'a>(&'a mut self) -> &'a str {
        &self.my_string
    }
}

请注意 (1),生命周期在 函数 上而不是在特征上。这是添加生命周期参数时的另一个好的经验法则,始终将它们放在尽可能最具体的范围内。还有(2),不需要两个参数。


编辑:

至于理解为什么它不特别喜欢你的版本,我认为这基本上是因为生命周期是在特征而不是函数上定义的。 MyTrait 上的生命周期 'b 特别是 类型 的特征,而不是任何特定代码路径的特征。就编译器所知,对于泛型 MyTrait<'a, 'b>,这些生命周期可能完全与代码中其他地方的其他内容相关联,它无法知道 'b 无法比您对 foo 的调用更长久因为它必须对所有可能的 'bs.

通用

但在 foo2 中,编译器可以看到 MyStruct 的所有生命周期,它可以将这些生命周期折叠到它拥有的具体情况,并且知道它只需要借用 s 持续 bar.