Почему происходит заимствование перемещенного значения при вызове метода, который принимает значение self по значению с аргументом, который также вызывает метод?

#rust #borrow-checker

#Ржавчина #заимствование-проверка

Вопрос:

Я столкнулся с проблемой, которая вынуждает меня разделить хороший oneliner на {} блок с промежуточным let значением. Причина этого мне вообще не ясна. Я смог изолировать проблему в этом минимальном примере:

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

impl AB {
    fn foo(amp;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(amp;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:

Порядок вычисления, с целью заимствования, слева направо.

Это означает, что объект bar вызова, упоминание о «перемещении» x , рассматривается перед упоминанием о «заимствовании» x в foo вызове, и, следовательно, компилятор считает, что переменная была перемещена из.

Для аналогичного случая, когда внешнее упоминание является изменяемым заимствованием, RFC 2025 был принят в качестве решения, но еще не был реализован. К сожалению, этот RFC, похоже, не охватывает ваш случай, где внешним использованием является перемещение.

Комментарии:

1. Значит, порядок вычисления проверки заимствования отличается от реального порядка выполнения?

2. Я думаю, что часть моей путаницы происходит от sugar, который является синтаксисом метода в стиле ООП. x(a,b,c) выглядит как функция, включающая 3 аргумента. Оценка слева направо имеет смысл здесь, но поскольку x находится «за скобками», я ошибочно посчитал это «не частью самого вызова функции». Но, действительно, рассматривая это как AB::bar(x, x.foo()) , становится ясно. Спасибо

3. Тогда почему это работает? let mut v = vec![0, 1, 2]; v.push(v.len());

4. @MathijsKwik Я не думаю, что «порядок оценки» — это правильный способ думать об этом: время жизни выводится , а не оценивается. Двухфазное заимствование исправляет amp;mut случай, разделяя заимствование на два с разным временем жизни, но перемещение не является заимствованием и не имеет времени жизни, поэтому оно действительно сильно отличается семантически. Тем не менее, я согласен, что похоже, что self -принимающий случай также может быть обработан аналогичным образом.

5. @nalzok При двухфазном заимствовании (что, чтобы было понятно, происходит в v.push(v.len()) ) amp;mut заимствование первоначально обрабатывается как общее заимствование, которое затем «обновляется» до полного изменяемого заимствования для вызова функции. На самом деле я не могу более точно изложить концепцию, чем в руководстве по разработке rustc , что также подтверждает ваше подозрение, что с v.push происходит что-то забавное; однако на этот раз это не Deref , а autoref .