Изменяемые ссылки на 'self' в объектных методах Rust

Перевод | Автор оригинала: Matt Oswalt

В последнее время я работаю над графическим программированием на Rust, как продолжение моих первых шагов с языком. В рамках этой работы я создал созданный мной тип Vec3f для хранения декартовых координат данного вектора:

#[derive(Copy, Clone, Debug)]
struct Vec3f {
    x: f32,
    y: f32,
    z: f32
}

В ходе естественной работы мне потребовалось добавить определенные методы для этого типа, чтобы я мог выполнять вычисления, такие как перекрестное произведение и скалярное / скалярное произведение. Эти функции довольно просты и читают информацию из экземпляра Vec3f (self), выполняют какие-то вычисления и возвращают какой-то результат, обычно новый экземпляр Vec3f или простой f32.

impl Vec3f {
    fn new(x: f32, y: f32, z: f32) -> Vec3f {
        Vec3f { x: x, y: y, z: z }
    }

    fn magnitude(self) -> f32 {
        self.dot(self).sqrt()
    }

    fn dot(self, other: Vec3f) -> f32 {
        self.x * other.x + self.y * other.y + self.z * other.z
    }
}

В некоторых случаях я хочу сделать больше. Например, распространенной задачей в графическом программировании является «нормализация» вектора, то есть активное изменение его свойств так, чтобы его направление не изменилось, но его величина уменьшилась до 1. Такой вектор также называется единичным вектором. .

Это делается путем умножения каждого свойства на результат деления 1,0 на исходную величину вектора. В своей первоначальной попытке я придумал следующее:

impl Vec3f {

   // We intend on mutating this instance of `Vec3f` in-place, so we want
   // to declare the `self` parameter with the `mut` keyword.
   fn normalize(mut self) {
        let mag = self.magnitude();

        // We're reading from a property of "self" to form part of the calculation,
        // and feeding the result back into the appropriate property.
        self.x = self.x * (1.0 / mag);
        self.y = self.y * (1.0 / mag);
        self.z = self.z * (1.0 / mag);
    }
}

Чтобы продемонстрировать, мы можем вызвать эту функцию просто, сначала создав экземпляр Vec3f с именем v, с некоторыми выдуманными координатами, а затем вызвав его метод normalize(), который должен изменить координаты на месте, чтобы гарантировать, что вектор нормализован.

fn main() {
  let mut v = Vec3f::new(1., 2., 3.);
  v.normalize();
  println!("{:?}", v);
}

Однако вывод, показанный оператором println, похоже, указывает на то, что что-то не так:

Vec3f { x: 1.0, y: 2.0, z: 3.0 }

По какой-то причине координаты нашего вектора не изменились. Чтобы начать устранение этой проблемы, я добавил оператор отладки в конец функции normalize(), и кажется, что свойства координат для self действительно были изменены в этом месте. Однако наш оператор отладки в функции main() не отображает эти изменения - он по-прежнему показывает исходные значения без изменений.

Что дает?!?

Право собственности снова забастовало

Оказывается, это еще один случай, когда модель собственности Rust пытается удержать меня от совершения глупостей.

Первое, что напомнило мне о проблеме, - это предупреждение компилятора:

warning: variable does not need to be mutable
  --> examples/blogexample.rs:30:7
   |
30 |   let mut v = Vec3f::new(1., 2., 3.);
   |       ----^
   |       |
   |       help: remove this `mut`

Мне было странно, что Rust говорил мне, что мне не нужно объявлять это изменяемым. Функция normalize() обязательно должна изменять v - в этом вся ее цель. Так что это ключевое слово должно быть необходимо.

Вы можете заметить, что я аннотирую свой тип Vec3f для автоматического получения некоторых реализаций, а именно:

#[derive(Copy, Clone, Debug)]

Если мы удалим это и попытаемся скомпилировать, вы сразу поймете, почему:

error[E0382]: use of moved value: `self`
  --> examples/blogexample.rs:15:18
   |
14 |     fn magnitude(self) -> f32 {
   |                  ---- move occurs because `self` has type `Vec3f`, which does not implement the `Copy` trait
15 |         self.dot(self).sqrt()
   |         ----     ^^^^ value used here after move
   |         |
   |         value moved here

error[E0382]: use of moved value: `self.z`
  --> examples/blogexample.rs:22:18
   |
18 |     fn normalize(mut self) {
   |                  -------- move occurs because `self` has type `Vec3f`, which does not implement the `Copy` trait
19 |         let mag = self.magnitude();
   |                   ---- value moved here
...
22 |         self.z = self.z * (1.0 / mag);
   |                  ^^^^^^ value used here after move

Перед попыткой реализовать функцию normalize() я добавил эту аннотацию, чтобы мы могли беспрепятственно использовать свойства Vec3f для вычислений. До сих пор нам просто нужно было вернуть новые значения, такие как тип f32, на основе вычислений, которые мы можем получить, просто прочитав свойства вектора. Нам не нужно было их менять, просто прочтите их.

Это методы объекта, которые используют первый параметр self (очень похоже на то, как это делает Python). Одно из правил владения Rust заключается в том, что у ценности может быть только один владелец. Поскольку тип Vec3f изначально не имел метода для копирования или клонирования самого себя (что имеет место для любого типа без аннотации), он переместил право собственности на значение в метод.

Из-за такого поведения любой код после этого перемещения не может продолжать использовать значение. Мы даже не можем использовать макрос println для печати результата после функции normalize():

error[E0382]: borrow of moved value: `v`
  --> examples/blogexample.rs:33:20
   |
31 |   let v = Vec3f::new(1., 2., 3.);
   |       - move occurs because `v` has type `Vec3f`, which does not implement the `Copy` trait
32 |   v.normalize();
   |   - value moved here
33 |   println!("{:?}", v);
   |                    ^ value borrowed here after move

Итак, очевидно, что нам нужна функция копирования, по крайней мере, так, как это реализовано в настоящее время. Это предоставляет компилятору альтернативу перемещению владения этими значениями в область, где остальная часть нашего кода остается высыхать.

На помощь приходят изменчивые отсылки!

Итак, мы выяснили, что для компиляции нашего кода нам нужно аннотировать наши структуры, чтобы мы автоматически получали для них возможности копирования / клонирования. Однако это не решает нашу первоначальную проблему, заключающуюся в том, что наш объектный метод не выглядел на самом деле изменяющим наш экземпляр Vec3f, как мы хотели.

Итак, поскольку теперь мы знаем, что в контексте этого метода self на самом деле является копией этого значения, внезапно становится очевидным, что все, что мы делаем, - это изменяем это скопированное значение, а не исходный экземпляр, который по-прежнему принадлежит переменная v в нашей функции main().

Есть другой способ, который не требует перемещения или копирования, и это объявить self в этой функции как изменяемую ссылку. Мы можем сделать это, добавив амперсанд перед ключевым словом mut:

fn normalize(&mut self) {
    let mag = self.magnitude();
    self.x = self.x * (1.0 / mag);
    self.y = self.y * (1.0 / mag);
    self.z = self.z * (1.0 / mag);
}

До этого изменения, поскольку self было копией значения, все, что мы делали, это заявляли, что хотим иметь возможность видоизменить эту копию. Добавляя амперсанд, мы позволяем функции нормализации фактически заимствовать право собственности на исходное значение. Теперь вместе с ключевым словом mut мы можем вносить изменения. Повторный запуск этого примера показывает нормализованный вектор:

Vec3f { x: 0.26726124, y: 0.5345225, z: 0.8017837 }

Обратите внимание, что Rust допускает только одну изменяемую ссылку на переменную за раз, но, к счастью, это все, что нам нужно. Наша функция нормализации заимствует ссылку, вносит быстрые изменения и возвращает ее. Вызывающий код блокируется до тех пор, пока это не будет сделано, после чего область, в которой была создана изменяемая ссылка, исчезнет.

Когда-нибудь я узнаю

Я все еще новичок в Rust, и должен сказать, что я прочитал главу о владении и заимствовании несколько раз, и не думаю, что действительно «понял», пока эта проблема не укусила меня. Нет ничего лучше нескольких боевых шрамов, чтобы по-настоящему усвоить трудные уроки! :)

Надеюсь, это вам помогло.