为什么 Rust 结构体可以借用 "&'a mut self" 两次,但 trait 不行?

24
以下 Rust 代码编译成功
struct StructNothing;

impl<'a> StructNothing {
    fn nothing(&'a mut self) -> () {}

    fn twice_nothing(&'a mut self) -> () {
        self.nothing();
        self.nothing();
    }
}

然而,如果我们试图将其打包成一个特质,它会失败

pub trait TraitNothing<'a> {
    fn nothing(&'a mut self) -> () {}

    fn twice_nothing(&'a mut self) -> () {
        self.nothing();
        self.nothing();
    }
}

这给我们带来了:
error[E0499]: cannot borrow `*self` as mutable more than once at a time
 --> src/lib.rs:6:9
  |
1 | pub trait TraitNothing<'a> {
  |                        -- lifetime `'a` defined here
...
5 |         self.nothing();
  |         --------------
  |         |
  |         first mutable borrow occurs here
  |         argument requires that `*self` is borrowed for `'a`
6 |         self.nothing();
  |         ^^^^ second mutable borrow occurs here
  • 为什么第一种版本被允许,而第二种版本被禁止?
  • 有没有办法说服编译器第二个版本是可以的?

背景和动机

rust-csv这样的库希望支持流式、零拷贝解析,因为根据基准测试,它比分配内存快25到50倍。但 Rust 的内置 Iterator 特性不能用于此,因为无法实现collect()方法。 目标是定义一个 StreamingIterator 特性,可由 rust-csv 和几个类似的库共享,但迄今为止,每次尝试实现它都遇到了上述问题。


2
fn nothing(&'a mut self)更改为fn nothing(&mut self)可以解决这个问题。考虑到你的函数没有返回值,你真的需要这个生命周期指定符吗?然而,这看起来确实是一个bug。 - Levans
3
Levans: 是的,如果没有生命周期指定符,这个设计的其余部分就会崩溃。但如果我们能够让生命周期指定符正常工作,我们就可以构建一个相当不错的“StreamingIterator”库。这只是一个让我们困惑不解的最小示例。 - emk
3
我认为这个问题可以通过类似HRL(higher rank lifetimes)的方式来解决,其中你可以拥有(假设的语法)trait StreamingIterator<T<'*>> { fn next<'a>(&'a mut self) -> T<'a>; }。不过目前我们不能准确地表达这个意思。 - huon
指出了我误用术语:上面应该说“HKL(高级生命周期)”。 - huon
4个回答

3
以下是Francis答案的扩展,使用隐式生命周期,但允许返回值有生命周期限制:
pub trait TraitNothing<'a> {
    fn change_it(&mut self);

    fn nothing(&mut self) -> &Self {
        self.change_it();
        self
    }

    fn bounded_nothing(&'a mut self) -> &'a Self {
        self.nothing()
    }

    fn twice_nothing(&'a mut self) -> &'a Self {
        // uncomment to show old fail
        // self.bounded_nothing();
        // self.bounded_nothing()
        self.nothing();
        self.nothing()
    }
}

虽然不完美,但你可以在其他方法中多次调用隐式生命周期的方法change_itnothing。我不知道这是否能解决你的实际问题,因为在特质方法中,self具有通用类型&mut Self,而在结构体中,它具有类型&mut StructNothing,编译器无法保证Self不包含引用。这种解决方法确实解决了代码示例。


2

如果您将生命周期参数放在每个方法上而不是特质本身上,它就可以编译

pub trait TraitNothing {
    fn nothing<'a>(&'a mut self) -> () {}

    fn twice_nothing<'a>(&'a mut self) -> () {
        self.nothing();
        self.nothing();
    }
}

不幸的是,这并不允许返回值被绑定到'a,而这正是@emk试图解决的真正问题所需的。 - huon
1
我知道这里有一个陷阱。但是,为什么呢?另外,我刚刚尝试在特质上放置<'a>,并在方法上放置<'b: 'a>,但我只是回到了最初的问题。在特质上使用<'a>实际上意味着什么? - Francis Gagné

0

似乎没有人回答“为什么?”所以我来了。

关键在于:在特质中,我们调用了来自同一特质的方法。然而,在自由实现中,我们没有调用来自同一实现的方法

什么?我们肯定是从同一实现中调用方法吧?

让我们更加精确:我们从同一实现中调用方法,但不是使用相同的泛型参数

您的自由实现基本上等同于以下内容:

impl StructNothing {
    fn nothing<'a>(&'a mut self) {}

    fn twice_nothing<'a>(&'a mut self) {
        self.nothing();
        self.nothing();
    }
}

由于impl的通用lifetime是浮动的,因此可以针对每个方法单独选择。编译器不会调用<Self<'a>>::nothing(self)>,而是调用<Self<'some_shorter_lifetime>>::nothing(&mut *self)>。

然而,使用Trait时情况完全不同。我们唯一能确定的是Self: Trait<'b>。我们不能用较短的lifetime调用nothing(),因为也许Self并没有用较短的lifetime实现Trait。因此,我们被迫调用<Self as Trait<'a>>::nothing(self)>,结果是我们正在借用重叠区域。

由此可以推断,如果我们告诉编译器Self实现了任何lifetime的Trait,它就能正常工作:

fn twice_nothing(&'a mut self)
where
    Self: for<'b> TraitNothing<'b>,
{
    (&mut *self).nothing();
    (&mut *self).nothing();
}

...除了因为问题#84435而无法编译,所以我不知道这是否会成功:(


-4

这真的令人惊讶吗?

您所做的断言是,&mut self 至少持续了 'a 的生命周期。

在前一种情况下,&mut self 是一个指向结构体的指针。因为借用完全包含在 nothing() 中,所以没有指针别名。

在后一种情况下,&mut self 是一个指向指向结构体的指针+ trait vtable 的指针。你正在锁定实现 TraitNothing 的指向结构体,在整个函数执行期间(即每次)都会持续 'a

通过移除 'a,您隐含地使用了 'static,这意味着实现将永久持续,因此没问题。

如果要解决问题,请将 &'a TraitNothing 转换为 &'static TraitNothing……但我很确定这不是您想要做的。

这就是为什么我们需要 Rust 中的块作用域 ('b: { .... }) ……

也许尝试使用虚拟生命周期?


3
在trait声明中,&mut self并不是一个trait对象,它只是一个指向实现该trait的任何值的普通指针。即使它是一个&mut TraitNothing trait对象,它也只是一个指向结构体和虚函数表的指针,没有额外的间接层级。 - huon
@dbaupp 真的吗?我以为 trait &self 的实现是通过类似于 http://is.gd/Gl20OQ 的方式完成的。 - Doug
1
@Doug,不是的;方法的默认实现与在每个“impl”中编写该代码相同,包括静态分派。 - huon

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