Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Автоматическая реализация трэйтов

В качестве вводной давайте рассмотрим следующий пример: мы попытаемся создать структуру для хранения координат точки в двухмерном пространстве и попробуем сравнить два экземпляра точки на равенство:

struct Point2D { x: i32, y: i32 }

fn main() {
  let p1 = Point2D {x: 1, y: 1};
  let p2 = Point2D {x: 1, y: 1};
  println!("p1 = p2: {}", p1 == p2);
}

При попытке скомпилировать этот код мы получим ошибку:

error[E0369]: binary operation `==` cannot be applied to type `Point2D`
 --> src/my_module/num.rs:6:30
  |
6 |   println!("p1 = p2: {}", p1 == p2);
  |                           -- ^^ -- Point2D
  |                           |
  |                           Point2D

Компилятор указывает, что для того чтобы иметь возможность сравнивать объекты на равенство, их тип должен реализовывать трэйт PartialEq, который предоставляет метод для сравнения двух экземпляров на равенство.

Note

Пусть вас не смущает, что трэйт называется “Partial Equals” (частичное равенство): он используется именно для сравнения на равенство, а эта самая “частичность” задействуется только для редких случаев. Например, при сравнении двух f32, оба из которых равны NaN: спецификация IEEE-754 требует, чтобы результат сравнения двух NaN был ложным, несмотря на то, что они одинаковы.

Сам трэйт PartialEq имеет следующий вид (на самом деле он немного сложнее, но суть мы передали):

#![allow(unused)]
fn main() {
pub trait PartialEq {
    fn eq(&self, other: &Self) -> bool;
    fn ne(&self, other: &Self) -> bool { !self.eq(other) }
}
}

Как мы видим, трэйт содержит два метода: eq (equal) и ne (not equal). Когда компилятор встречает сравнение двух объектов при помощи оператора ==, он подменяет использование оператора == на вызов метода eq. То есть вызов p1 == p2 будет заменён на p1.eq(p2). Аналогично, применение оператора != заменяется на вызов метода ne.

Таким образом, чтобы проверка на равенство работала для нашей структуры Point2D, мы должны реализовать для неё PartialEq:

struct Point2D { x: i32, y: i32 }

impl PartialEq for Point2D {
    fn eq(&self, other: &Self) -> bool {
        self.x == other.x && self.y == other.y
    }
}

fn main() {
  let p1 = Point2D {x: 1, y: 1};
  let p2 = Point2D {x: 1, y: 1};
  println!("p1 = p2: {}", p1 == p2);
}

Теперь всё работает.

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

К счастью, в Rust есть специальный механизм, который позволяет генерировать вот такие наиболее частые реализации для трэйтов — аннотация derive.

derive

Если над нашей структурой мы “повесим” аннотацию #[derive(PartialEq)], то компилятор сам сгенерирует реализацию PartialEq для нашего типа. Автоматическая реализация метода eq просто сравнивает все соответствующие поля для объектов структуры, что нам и нужно.

#[derive(PartialEq)]
struct Point2D { x: i32, y: i32 }

fn main() {
  let p1 = Point2D {x: 1, y: 1};
  let p2 = Point2D {x: 1, y: 1};
  println!("p1 = p2: {}", p1 == p2);

  let p3 = Point2D {x: 0, y: 0};
  let p4 = Point2D {x: 1, y: 1};
  println!("p3 = p4: {}", p3 == p4);
}

Как видите, такая реализация работает правильно, при этом наш код стал заметно короче и выразительнее.

Note

Посмотреть, какой код реализации генерируется на основании аннотации derive, можно при помощи утилиты cargo expand.

Аннотации

Мы уже обсудили аннотацию derive, но пока что мы толком не знакомы с самими аннотациями.

Аннотация — это специальная пометка для компилятора, которой может быть отмечена структура, функция, модуль и т.д.

Аннотация имеет следующий синтаксис:

#[аннотация(аргументы аннотации)]

Когда компилятор встречает аннотацию, он обращается к соответствующему обработчику, который выполняет ту или иную кодогенерацию.

Соответственно, для аннотации derive в стандартной библиотеке Rust имеется специальный обработчик, который во время компиляции генерирует стандартную реализацию для тех или иных трейтов.

Кроме PartialEq, генерация стандартной реализации поддерживается для целого ряда других трэйтов из стандартной библиотеки, например:

  • Hash — стандартный трэйт, предоставляющий метод для вычисления хеш-кода объекта.
  • Debug — трэйт, который декларирует “отладочный” метод преобразования в строку. Именно он используется, когда мы распечатываем объект через {:?} в вызове println!.
  • Default — позволяет создавать значение по умолчанию для множества типов. Например: 0 — для чисел, false — для булевого типа, пустая строка — для строк, и т.д.

трэйт Clone

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

Однако если мы хотим не перемещать объект, а скопировать, то нам на помощь придёт трэйт Clone, который предоставляет для этого метод clone(). Сам трэйт выглядит вот так:

trait Clone: Sized {
    fn clone(&self) -> Self;
}

Казалось бы, что в нём особенного? Мы и сами можем написать для нашего типа метод, который будет возвращать копию объекта. Например, так:

#[derive(Debug)]
struct Point2D { x: i32, y: i32 }

impl Point2D {
    fn make_clone(&self) -> Point2D {
        Point2D { x: self.x, y: self.y }
    }
}

fn main() {
    let p1 = Point2D { x: 1, y: 1};
    let p2 = p1.make_clone();

    println!("p1={:?}, p2={:?}", p1, p2);
   // Напечатает: p1=Point2D { x: 1, y: 1 }, p2=Point2D { x: 1, y: 1 }
}

Однако вся прелесть в том, что для Clone также можно сгенерировать реализацию просто путём добавления аннотации derive.

#[derive(Debug,Clone)]
struct Point2D { x: i32, y: i32 }

fn main() {
    let p1 = Point2D { x: 1, y: 1};
    let p2 = p1.clone();

    println!("p1={:?}, p2={:?}", p1, p2);
   // Напечатает: p1=Point2D { x: 1, y: 1 }, p2=Point2D { x: 1, y: 1 }
}

Note

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

Разумеется, при необходимости ничто не мешает нам реализовать Clone для нашего типа вручную.

#[derive(Debug)]
struct Point2D { x: i32, y: i32 }

impl Clone for Point2D {
    fn clone(&self) -> Point2D {
        Point2D {x: self.x, y: self.y}
    }
}

fn main() {
    let p1 = Point2D { x: 1, y: 1};
    let p2 = p1.clone();
    println!("p1={:?}, p2={:?}", p1, p2);
}

Реализация clone(), сгенерированная при помощи #[derive(Clone)], делает глубокую копию объекта, т.е. текущий объект и все вложенные. Именно поэтому, если мы хотим применить #[derive(Clone)] для нашей структуры, то типы всех полей этой структуры также должны реализовать трэйт Clone.

трэйт Copy

Из главы Владение мы знаем, что операция присваивания, перемещает только объекты составных типов, а значения примитивных типов просто копируются.

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

trait Copy: Clone { }

Как мы видим, этот трэйт наследует трэйт Clone, но не добавляет никаких новых методов. Такие трэйты называются маркерными, т.е. просто играют роль “пометки” для компилятора.

Если компилятор видит, что тип объекта, используемого в операции присваивания, реализует трэйт Copy, то вместо того, чтобы перемещать объект, он вызывает на нём метод .clone() и присваивает полученную копию.

Перепишем наш пример для структуры Point2D, добавив трэйт Copy в аннотацию derive.

#[derive(Debug,Clone,Copy)]
struct Point2D { x: i32, y: i32 }

fn main() {
  let p1 = Point2D { x: 1, y: 1};
  let p2 = p1; // При присваивании вызывается p1.clone()

  println!("p1={:?}, p2={:?}", p1, p2);
  // p1=Point2D { x: 1, y: 1 }, p2=Point2D { x: 1, y: 1 }
}

Теперь при присваивании переменной p1 в переменную p2, перемещение владения не происходит. Вместо этого создаётся копия объекта из переменной p1 и присваивается переменной p2.