[Из песочницы] 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(param: T) {
...
}
Как же выбрать ограничение для T
? Если нам принципиальна полная эквивалентность, а без неё функция развалится, запишем так:
fn foo(param: T) {
...
}
но в таком случае в эту функцию нельзя будет передать к примеру f32
: как мы видели раньше, он поддерживает только PartialEq
.
Если такие ограничения не существенны, и функция стабильная при любых параметрах, тогда семантика будет выглядеть так:
fn bar(param: T) {
...
}
Эта функция с меньшими ограничениями (поиграть можно тут)
Из того, что Eq
на одно ограничение больше, чем PartialEq
, и не отличается от него никак в «меньшую» по ограничениям сторону, вытекает следующее важное заключение:
Все структуры, реализующие
Eq
, так же должны реализовыватьPartialEq
То есть запись #[derive(Eq)]
не правомочна, если на структуре не определён PartialEq
— выдаст ошибку!
Самый простой способ этого избежать:
#[derive(Eq, PartialEq)]
enum Foo {
...
}
Вспомним нашу функцию fn foo
из предыдущего примера. По семантике в ней 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
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
, структуры с PartialEq
— PartialOrd
.
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: использовать в связке с Mul —
x * T::one() == x
Для Zero: использовать в связке с Add —x + T::zero() == x
Эти трейты можно объявлять с помощью derive
, если все члены структур так же поддерживают One
или Zero
.
Вывод: если вы не исключаете возможности того, что ваши публичные структуры когда-нибудь будут использоваться в векторных пространствах, как то участвовать во всяких там алгебрах, то имеет смысл реализовать для структур эти трейты. Но пока что любое упоминание этих трейтов вызывает у компилятора только ругань…
Если в вашу структуру, реализующую какой-то Trait
через derive
попадёт поле, не реализующее этот трейт, то ваш код развалится. А если из-за этого вы уберёте реализацию вашего трейта — то код развалится у людей, использующих ваши структуры. Поэтому золотое правило:
Чем больше трейтов, тем больше ответственность.