当调用一个以值传递self并且带有一个调用了方法的参数的方法时,为什么会出现moved value的借用?

11

我遇到了一个问题,被迫将一个漂亮的一行代码拆分成带有中间let{}块。我完全不清楚这样做的原因。我通过这个最小的示例来隔离这个问题:

struct AB {
    a: u8,
    b: u8,
}

impl AB {
    fn foo(&self) -> String {
        String::from("foo")
    }
    fn bar(self, x: String) -> String {
        format!("{} - {} - {}!", x, self.a, self.b)
    }
}

fn main() {
    let x = AB { a: 3, b: 5 };
    let result = x.bar(x.foo());
    println!("{}", result);
}

我曾认为 bar 函数的参数会在调用 bar 之前被评估。在其执行期间,foo 借用了 x ,但当它返回其 String 时,借用已经结束了,因为 String 不是承载 x 生命周期的引用。当调用 bar 时,foo 的借用应该已经结束。

然而,编译器并不这样认为:

error[E0382]: borrow of moved value: `x`
  --> src/main.rs:17:24
   |
17 |     let result = x.bar(x.foo());
   |                  -     ^ value borrowed here after move
   |                  |
   |                  value moved here
   |
   = note: move occurs because `x` has type `AB`, which does not implement the `Copy` trait

我不反对bar确实移动了x这一事实。我的问题在于foo借用了x后移动发生的陈述。

一个简单(但丑陋)的修复:

struct AB {
    a: u8,
    b: u8,
}

impl AB {
    fn foo(&self) -> String {
        String::from("foo")
    }
    fn bar(self, x: String) -> String {
        format!("{} - {} - {}!", x, self.a, self.b)
    }
}

fn main() {
    let x = AB { a: 3, b: 5 };
    let y = x.foo();
    let result = x.bar(y);
    println!("{}", result);
}

x.foo()的赋值分配给一个中间变量y可以编译通过,证实了我的预期,即一旦foo返回,借用确实结束了,但是为什么会这样呢?我是否对评估顺序有所不理解?为什么我不能摆脱中间的let y

1个回答

9

为了借用目的,评估顺序是从左到右。

这意味着在foo调用中,“move-out”提及x的主题——bar调用的主题,先于对x的“borrow”提及考虑,因此,编译器认为该变量已被移动。

对于外部提及是可变借用的类似情况,RFC 2025已被接受作为解决方案,但尚未实现。不幸的是,这个RFC似乎没有涵盖您的情况,其中外部使用是移动操作。


3
我觉得我的困惑部分来源于面向对象编程(OOP)的方法语法中的糖语法。x(a,b,c)看起来像一个涉及三个参数的函数。从左到右的计算顺序在这里是有意义的,但由于x在“括号外面”,我错误地将其视为“不属于函数调用本身的一部分”。但通过将其确实视为 AB::bar(x, x.foo()) 可以使其更加清晰明了。 - Mathijs Kwik
2
为什么这能够正常工作呢? let mut v = vec![0, 1, 2]; v.push(v.len()); - Boiethios
1
@MathijsKwik 我认为“评估顺序”不是正确的思考方式:生命周期是推断的,而不是评估的。两阶段借用通过将借用分成具有不同生命周期的两个部分来修复&mut情况,但移动不是借用,也没有生命周期,因此在语义上真正不同。然而,我同意看起来self-taking情况也可以以类似的方式处理。 - trent
@trentcl 两阶段借用通过将可变引用分成两个具有不同生命周期的子引用来解决了&mut情况。 您能详细说明一下原始&mut v借用被拆分为哪两个子借用吗?PS:我注意到尽管v.push(v.len())可以编译,但Vec::push(&mut v, v.len())不能。幕后必须有一些“Deref”魔术。 - nalzok
1
在双阶段借用中(明确一下,这就是v.push(v.len())发生的情况),&mut借用最初被视为共享借用,然后被“升级”为完全可变借用,以便调用函数。我无法比rustc dev guide更好地解释这个概念,它也证实了你对v.push有点奇怪的猜测;但是,这次不是Deref,而是自动引用。 - trent
显示剩余3条评论

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