Traits из коробки

в 7:03, , рубрики: Rust, traits, ооп

В стандартной библиотеке языка Rust есть несколько трейтов, которые можно объявить "на халяву" с помощью derive. Эти трейты обязательно пригодятся при объявлении собственных структур, они очень часто встречаются в различных open-source библиотеках, но их реализация генерируется компилятором и может вызывать вопросы.
Часто видите:

#[derive(RustcEncodable, RustcDecodable, Clone, Eq, Default)]
struct Foo {
}

и не понимаете, что это и где?

Компилятор сам может предоставить вам простые "встроенные" реализации для некоторых трейтов в помощью #[derive]. Конечно же, эти самые трейты можно реализовать и вручную, если требуется более сложное поведение.

Итак, вот примерный список трейтов, которые можно "извлечь"(derive переводится именно так):

  • [↓] Трейты сравнения: Eq, PartialEq, Ord, PartialOrd
  • [↓] Clone и Copy — трейты, отвечающие за клонирование и копирование.
  • [↓] Hash — для расчёта хеша &T
  • [↓] Default — позволяет задать "значение по умолчанию"
  • [↓] Debug — определяет формат вывода значения структуры при использовании {:?} форматтера

Кроме того, нестабильные:

  • [↓] Zero, One — задают значения единицы и нуля у "числовых" структур

Все вышеперечисленные трейты лежат в стандартной библиотеке std. Кроме того, можно найти собственноручно сделанные библиотеки, где так же есть трейты, извлекающиеся с помощью #[derive]. Примером служит часто используемые RustcEncodable/RustcDecodable. Можно реализовать поддержку derive и для своих трейтов с помощью очень хитрых макросов (которые скорее всего в половине случаев не будут работать :) ), но это выходит за рамки нашей статьи.

Рассмотрим вышеперечисленные трейты

Трейты сравнения

Позволяют устроить для своих структур модель отношений эквивалентности. В последствии структуры можно будет сравнивать, упорядочивать, структуризировать и организовывать. Рассмотрим их подробнее

Eq и PartialEq

Два похожих трейта, отвечают за то, чтобы можно было сказать foo == bar или нет. Но зачем на такое простое дело два разных трейта? А разгадка вот в чём:

PartialEq не гарантирует равенство себя с самим: не факт, что a == a

Как такое может быть? К примеру, у чисел NaN != NaN

use std::f32;

fn main() {
    let a = f32::NAN;
    let b = f32::NAN;
    println!("{}", a == b);
}

вернёт false

Представим себя создателем библиотеки, в которой есть какая то функция сравнения:

fn foobar<T>(param: T) {
  ...
}

Как же выбрать ограничение для T? Если нам принципиальна полная эквивалентность, а без неё функция развалится, запишем так:

fn foo<T: Eq>(param: T) {
  ...
}

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

fn bar<T: PartialEq>(param: T) {
  ...
}

Эта функция с меньшими ограничениями (поиграть можно тут)

Из того, что Eq на одно ограничение больше, чем PartialEq, и не отличается от него никак в "меньшую" по ограничениям сторону, вытекает следующее важное заключение:

Все структуры, реализующие Eq, так же должны реализовывать PartialEq

То есть запись #[derive(Eq)] не правомочна, если на структуре не определён PartialEq - выдаст ошибку!

Самый простой способ этого избежать:

#[derive(Eq, PartialEq)]
enum Foo {
  ...
}

Вспомним нашу функцию fn foo<T: Eq>() из предыдущего примера. По семантике в ней T определён как Eq, но получается, что она автоматически принимает и PartialEq структуры.

Ещё одно свойство Eq и PartialEq — их нельзя автоматически извлечь для структур, хоть один элемент которых не реализует Eq и PartialEq соответственно. К примеру:

#[derive(Eq, PartialEq)]
struct Foo {
  bar: f32
}

выдаст ошибку, потому что, как мы уже выяснили, f32 не реализует Eq.

Точно так же не будет работать и такой код:

struct Foo {}

#[derive(PartialEq)]
struct Bar {
  foo: Foo
}

потому что наша структура Foo не реализует PartialEq (поиграть можно тут)

Впрочем, такое поведение соответствует всем derivable трейтам. Это мы увидим далее

Вывод: при написании кода будет разумно реализовывать Eq (там где можно) или как минимум PartialEq во всех публичных структурах — пригодится! И спасёт человека, использующего ваш код от внезапных ошибок компиляции, когда он решит сравнить парочку объектов, или позволит ему включить вашу структуру в свою, реализующую сравнение, без лишних бубнов!

Ord и PartialOrd

Опять два похожих трейта, только в этот раз отвечающие за "больше-меньше". Однако отличаются они сильнее, чем Eq и PartialEq:

Трейт PartialOrd добавляет функции сравнения: >, <, >=, <=, !=, = и реализует нестрогое упорядочивание: Option<Ordering>

use std::cmp::Ordering;

let result = 1.0.partial_cmp(&2.0);
assert_eq!(result, Some(Ordering::Less));

let result = 1.0.partial_cmp(&1.0);
assert_eq!(result, Some(Ordering::Equal));

let result = 2.0.partial_cmp(&1.0);
assert_eq!(result, Some(Ordering::Greater));

let result = std::f64::NAN.partial_cmp(&1.0);
assert_eq!(result, None);

Трейт Ord реализует "строгое" упорядочивание: Ordering

use std::cmp::Ordering;

assert_eq!(5.cmp(&10), Ordering::Less);
assert_eq!(10.cmp(&5), Ordering::Greater);
assert_eq!(5.cmp(&5), Ordering::Equal);

Если посмотреть на зависимости этих трейтов, то всё становится понятно:

Для реализации Ord, структура должна реализовывать Eq.
Для реализации PartialOrd, структура должна реализовывать PartialEq

Действительно, разве можно точно сказать, "больше" или "больше или равно" значение относительно второго, если может оказаться, что структура не равна самой себе?

Заметим так же, что Ord требует реализации PartialOrd, поэтому все операции сравнения для таких структур будут работать гарантированно. В общем, дальше — полная аналогия с Eq и PartialEq.
Поиграть со всем этим хозяйством можно вот тут.

Вывод: по аналогии с Eq и PartialEq — стараемся реализовать Ord или хотя бы PartialOrd во всех структурах, где это можно и логично. Соответственно, структуры с Eq смогут поддержать и Ord, структуры с PartialEqPartialOrd.

Clone и Copy

Даже в русском языке слова "клонирование" и "копирование" — синонимы с трудноразличимой разницей. А вот в языке Rust — это разные вещи. Сейчас с ними и разберёмся

Clone позволяет создать T из &T с помощью копирования.
Copy позволяет скопировать, а не "переместить" значение переменной при присваивании.

Короче говоря, эти трейты отвечают вообще за разные вещи, связаны они только тем что:

Для реализации Copy, структура должна реализовывать Clone.

Clone по сути — обычная вещь. Она реализована практически для всего, что только можно предположить. Да и копировать объект путём возвращения из функции clone() сгенерированной копии объекта — не такая большая проблема, как бы такой объект не хранился в памяти — создаём новый объект, приводим его к такому же виду, возвращаем.

Copy — другая песня. По сути, "копия" здесь — это побайтовый перенос объекта один в один. И такую функциональность поддерживают далеко не все структуры. К примеру при "тупом" копировании указателя создастся два указателя, показывающие на одно и то же место, что полностью противоречит "безопасной" идеологии Rust. Или к примеру, объекты, хранящие какие-то метаданные, также не могут быть скопированы побайтово.

Однако, Copy позволяет проще разбираться с семантикой переноса.

#[derive(Debug, Copy, Clone)]
struct Foo;

#[derive(Debug, Clone)]
struct Bar;

fn main(){    
    let foo = Foo;
    let bar = foo;
    println!("foo is {:?} and bar is {:?}", foo, bar);

    let foo = Bar;
    let bar = foo;
    println!("foo is {:?} and bar is {:?}", foo, bar);
}

Первый println! сработает, второй — нет. А разница — в Copy.
Поиграть с этим можно тут.

Вывод: как советуют в официальных доках, реализуем Copy вообще везде, где только можно. А нельзя его, в общем случае, реализовывать там, где реализуется Drop. Потому что, в общем случае, если для структуры надо вызывать деструктор, значит она сделана так, что простым слепком памяти её не скопируешь. Clone похоже, не помешает нигде.

Hash, Default и Debug

Трейт Hash отвечает за возможность взятия хэша из структуры. Это необходимое условие для того, чтобы можно было составить из таких структур HashMap или HashSet.

Трейт Default отвечает за начальные(по умолчанию) значения вновь созданной структуры. Структуры, реализующие Default часто требуются в различных библиотеках по работе с данными. Так же реализация этого трейта полезна для структур, характеризующих параметры какой то системы — всегда есть параметры по умолчанию, которые лень каждый раз писать :)

Трейт Debug отвечает за отображение структуры в виде текстовой отформатированной строки. Нигде не требуется, разве что в библиотеках логгирования, но бывает полезно при отладке собственных программ.

Трейты разные сами по себе, объединил их потому, что с derive они работают одинаково:

Hash, Default, и Debug можно реализовать в тех структурах, все члены которых поддерживают Hash, Default и Debug соответственно.

И этого не только необходимо, но и достаточно. Никакой лишней мороки, трейтов и чего бы то ни было ещё. Поиграть с этим хозяйством можно здесь.

Вывод: Hash можно реализовывать во всех публичных структурах — лишним не будет. Default и Debug — на ваш вкус, но желательно. Это поможет людям, использующим ваш код, не иметь проблем при включении ваших структур в свои. Короче говоря, если не жалко — набираем #[derive(...)] и сыпем туда всё от щедрот.

One и Zero

На день написания статьи (Rust 1.6 Stable) эти трейты объявлены как нестабильные. Однако, по всей видимости, в будущем они смогут определять векторные пространства для произвольных структур данных, при использовании вместе с операциями сложения(Add) и умножения(Mul). Для реализации этих трейтов, ваши структуры должны обладать следующими свойствами:

Для One: использовать в связке с Mulx * T::one() == x
Для Zero: использовать в связке с Addx + T::zero() == x

Эти трейты можно объявлять с помощью derive, если все члены структур так же поддерживают One или Zero.

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

Самый заключительный варнинг

Если в вашу структуру, реализующую какой-то Trait через derive попадёт поле, не реализующее этот трейт, то ваш код развалится. А если из-за этого вы уберёте реализацию вашего трейта — то код развалится у людей, использующих ваши структуры. Поэтому золотое правило:

Чем больше трейтов, тем больше ответственность.

Автор: Virviil

Источник

Поделиться новостью

* - обязательные к заполнению поля