Перейти к содержимому

Популярные трейты в Rust

На странице Трейты ты писал свои трейты. Но в стандартной библиотеке уже есть набор трейтов, которыми пользуется почти любая программа на Rust. Они отвечают на простые вопросы о типе: Можно ли его вывести на экран? Скопировать? Сравнить? Создать «пустое» значение?

Представь их как нашивки на форме. Каждая нашивка говорит: «этот тип умеет делать X». Многие из них достаются даром через #[derive(...)], другие пишутся вручную. На этой странице разобраны те, что встречаются чаще всего, а в конце есть таблица, к которой удобно возвращаться.

Debug позволяет выводить значение через {:?}. Он нужен тебе, разработчику, — чтобы смотреть на значения, пока пишешь и чинишь код. Вручную его почти никогда не пишут: просто выводят через derive. Для «красивой» версии, где каждое поле на своей строке, есть {:#?} — большие структуры так читать легче:

#[derive(Debug)]
struct Book {
title: String,
pages: u32,
}
fn main() {
let book = Book { title: String::from("Дюна"), pages: 412 };
println!("{:?}", book);
println!("{:#?}", book);
}

Вывод:

Book { title: "Дюна", pages: 412 }
Book {
title: "Дюна",
pages: 412,
}
▶ Попробуй сам

Макрос dbg!, который ты можешь встретить в чужом коде, тоже использует Debug.

Display — это то, что использует {}. Он нужен для текста, который видит пользователь, — сообщения, которое человек читает на экране. Угадать, как он должен выглядеть, Rust не может, поэтому derive для него нет. Ты пишешь его сам, реализуя fmt::Display.

Строки в этом примере английские: программа выводит длину текста через .len(), а она считает байты, и у кириллицы результат был бы другим.

use std::fmt;
struct Book {
title: String,
author: String,
}
impl fmt::Display for Book {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} by {}", self.title, self.author)
}
}
fn main() {
let book = Book {
title: String::from("Dune"),
author: String::from("Frank Herbert"),
};
println!("{}", book);
let text = book.to_string();
println!("That text has {} characters", text.len());
}

Вывод:

Dune by Frank Herbert
That text has 21 characters
▶ Попробуй сам

write! работает как println!, только пишет не на экран, а в форматтер f. Приятный бонус: как только у типа есть Display, ты бесплатно получаешь .to_string(). С Display ты ещё встретишься на странице Свои ошибки, где он даёт ошибкам понятное сообщение.

Оба создают копию значения, но по-разному.

  • Clone даёт метод .clone(). Ты вызываешь его сознательно, и он может делать настоящую работу — например, копировать весь текст внутри String.
  • Copy делает копию автоматически каждый раз, когда ты присваиваешь или передаёшь значение. Он разрешён только для маленьких простых типов, которые можно скопировать побитово: чисел, bool, char и структур только из них.
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
#[derive(Debug, Clone)]
struct User {
name: String,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Copy: p1 по-прежнему можно использовать
println!("{:?} и {:?}", p1, p2);
let u1 = User { name: String::from("Аня") };
let u2 = u1.clone(); // Clone: мы сами просим копию
println!("{:?} и {:?}", u1, u2);
}

Вывод:

Point { x: 1, y: 2 } и Point { x: 1, y: 2 }
User { name: "Аня" } и User { name: "Аня" }
▶ Попробуй сам

Без Copy строка let p2 = p1; переместила бы p1, и пользоваться им дальше было бы нельзя (см. Владение). Обрати внимание: Copy всегда требует ещё и Clone — поэтому их выводят вместе.

PartialEq позволяет использовать == и !=. Если вывести его через derive, два значения равны, когда равны все их поля:

#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
let c = Point { x: 5, y: 0 };
println!("a == b: {}", a == b);
println!("a != c: {}", a != c);
}

Вывод:

a == b: true
a != c: true
▶ Попробуй сам

А что такое Eq? Новых методов он не добавляет. Это обещание: «любое значение всегда равно самому себе». Звучит очевидно, но числа с плавающей точкой его нарушают: в Rust f64::NAN == f64::NAN — это false. Поэтому у f64 есть PartialEq, но нет Eq. Eq (вместе с Hash) нужен, чтобы использовать твой тип как ключ HashMap или хранить его в HashSet. Если в структуре нет полей с плавающей точкой, выводи оба.

PartialOrd позволяет использовать <, >, <= и >=. Ord добавляет полный порядок — он нужен для .sort(), .max() и .min(). Когда их выводят через derive, Rust сравнивает поля сверху вниз: сначала первое поле, и только если они равны — следующее.

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Version {
major: u32,
minor: u32,
patch: u32,
}
fn main() {
let old = Version { major: 1, minor: 2, patch: 0 };
let new = Version { major: 1, minor: 10, patch: 3 };
println!("old < new: {}", old < new);
let mut versions = vec![
Version { major: 2, minor: 0, patch: 0 },
new,
old,
];
versions.sort();
for v in &versions {
println!("{}.{}.{}", v.major, v.minor, v.patch);
}
}

Вывод:

old < new: true
1.2.0
1.10.3
2.0.0
▶ Попробуй сам

Раз поля сравниваются по порядку, порядок, в котором ты их пишешь, важен. Здесь самое важное поле — major, и для номеров версий это именно то, что нужно. У перечислений «меньше» считаются варианты, объявленные раньше. Как и с Eq, числа с плавающей точкой получают только PartialOrd, но не Ord; как их всё-таки отсортировать, показано на странице Сортировка.

Default даёт типу функцию ::default(), которая создаёт «начальное» значение. При выводе через derive каждое поле получает своё значение по умолчанию: 0 для чисел, false для bool, пустую String, пустой Vec. Синтаксис ..Default::default() позволяет задать несколько полей, а остальные заполнить по умолчанию:

#[derive(Debug, Default)]
struct Settings {
volume: u32,
muted: bool,
username: String,
}
fn main() {
let basic = Settings::default();
println!("{:?}", basic);
let custom = Settings {
volume: 80,
..Default::default()
};
println!("{:?}", custom);
}

Вывод:

Settings { volume: 0, muted: false, username: "" }
Settings { volume: 80, muted: false, username: "" }
▶ Попробуй сам

Если нужны другие начальные значения (скажем, громкость 50), не используй derive, а напиши impl Default for Settings сам, с функцией fn default() -> Self, которая возвращает нужные значения.

From описывает, как создать один тип из другого. Ты им уже пользовался: String::from("hi"). Когда ты реализуешь From, Rust бесплатно даёт тебе Into в обратную сторону:

struct Celsius(f64);
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
fn main() {
let boiling = Fahrenheit::from(Celsius(100.0));
println!("Вода кипит при {}°F", boiling.0);
let body: Fahrenheit = Celsius(37.0).into();
println!("Температура тела — {:.1}°F", body.0);
}

Вывод:

Вода кипит при 212°F
Температура тела — 98.6°F
▶ Попробуй сам

С .into() Rust должен знать целевой тип, поэтому мы написали let body: Fahrenheit.

Как ? использует From. Вот где From по-настоящему раскрывается. Когда оператор ? встречает ошибку, он вызывает From::from, чтобы превратить её в тип ошибки, который возвращает твоя функция. Так что если реализовать From для своего типа ошибки, ? будет преобразовывать ошибки за тебя:

use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
BadNumber(ParseIntError),
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::BadNumber(e)
}
}
fn parse_age(text: &str) -> Result<u32, AppError> {
let age: u32 = text.parse()?; // здесь ParseIntError превращается в AppError
Ok(age)
}
fn main() {
println!("{:?}", parse_age("42"));
println!("{:?}", parse_age("abc"));
}

Вывод:

Ok(42)
Err(BadNumber(ParseIntError { kind: InvalidDigit }))
▶ Попробуй сам

Подробнее эта идея развивается на странице Свои ошибки.

В Rust a + b — это на самом деле вызов метода add трейта std::ops::Add. Реализуй его для своего типа, и + просто заработает. type Output говорит, какой тип получается в результате сложения:

use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point { x: self.x + other.x, y: self.y + other.y }
}
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 3, y: 4 };
println!("{:?}", a + b);
}

Вывод:

Point { x: 4, y: 6 }
▶ Попробуй сам

Та же идея работает для Sub (-), Mul (*), Neg (унарный -) и AddAssign (+=). Используй это, когда смысл очевиден: сложить две точки или две суммы денег. Если читателю придётся гадать, что значит + для твоего типа, лучше обычный метод с понятным именем.

Короткая шпаргалка для обычной структуры:

Трейт Выводить? Что получаешь
Debug Почти всегда Вывод через {:?} и {:#?}
Clone Обычно .clone()
Copy Только для маленьких типов без полей String, Vec или Box Автоматические копии вместо перемещений
PartialEq Обычно == и !=
Eq Когда нет полей с плавающей точкой Нужен для ключей HashMap (вместе с Hash)
Hash Когда тип — ключ HashMap или элемент HashSet Хеширование
PartialOrd, Ord Когда у значений есть естественный порядок <, >, .sort(), .max()
Default Когда «пустое» значение имеет смысл ::default() и ..Default::default()
Display Вывести нельзя — пиши вручную Вывод через {} и .to_string()

Очень частая первая строка — #[derive(Debug, Clone, PartialEq)]. Добавляй остальные, когда понадобятся: компилятор сам скажет, если какого-то не хватает.

derive(Copy) для структуры со String.

#[derive(Clone, Copy)]
struct User {
name: String,
age: u32,
}
error[E0204]: the trait `Copy` cannot be implemented for this type
--> src/main.rs:2:8
|
1 | #[derive(Clone, Copy)]
| ---- in this derive macro expansion
2 | struct User {
| ^^^^
3 | name: String,
| ------------ this field does not implement `Copy`

Компилятор говорит, что трейт Copy нельзя реализовать для этого типа, потому что поле name не реализует Copy. String владеет памятью в куче, поэтому побитово её не скопировать. Убери Copy и вызывай .clone(), когда нужна копия.

derive(Eq) для структуры с числом с плавающей точкой.

#[derive(Debug, PartialEq, Eq)]
struct Product { name: String, price: f64 }
error[E0277]: the trait bound `f64: Eq` is not satisfied
--> src/main.rs:2:32
|
1 | #[derive(Debug, PartialEq, Eq)]
| -- in this derive macro expansion
2 | struct Product { name: String, price: f64 }
| ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`

Здесь компилятор сообщает, что f64 не реализует Eq. Оставь PartialEq и убери Eq. Или храни цену целыми копейками в u64 — для денег это ещё и надёжнее.

Создай структуру Task с полями priority: u8 и title: String. Выведи через derive трейты, которые нужны, чтобы отсортировать Vec<Task>, вывести каждую задачу через {:?} и сравнить две задачи через ==. Чем меньше число, тем срочнее задача, поэтому такие задачи должны идти первыми. Подсказка: .sort() нужен Ord, а Ord требует ещё Eq, PartialEq и PartialOrd. Поставь priority первым полем, чтобы оно сравнивалось первым.

Показать решение
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Task {
priority: u8,
title: String,
}
fn main() {
let mut tasks = vec![
Task { priority: 3, title: String::from("Полить цветы") },
Task { priority: 1, title: String::from("Заплатить за квартиру") },
Task { priority: 2, title: String::from("Ответить Сэму") },
];
let copy = tasks[1].clone();
println!("Та же задача? {}", copy == tasks[1]);
tasks.sort();
for task in &tasks {
println!("{:?}", task);
}
}

Вывод:

Та же задача? true
Task { priority: 1, title: "Заплатить за квартиру" }
Task { priority: 2, title: "Ответить Сэму" }
Task { priority: 3, title: "Полить цветы" }
▶ Попробуй сам

2. Деньги, которые выводятся и складываются

Заголовок раздела «2. Деньги, которые выводятся и складываются»

Создай структуру Money, которая хранит целые центы cents: u64. Реализуй From<u64>, чтобы работало 350.into(), Display, чтобы сумма выводилась как $3.50, и Add, чтобы две суммы складывались через +. Подсказка: доллары — это cents / 100, а остаток — cents % 100. Выводи остаток через {:02}, чтобы 5 центов показывались как 05. Выведи через derive ещё и Copy, чтобы после сложения суммы можно было вывести.

Показать решение
use std::fmt;
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Money {
cents: u64,
}
impl From<u64> for Money {
fn from(cents: u64) -> Self {
Money { cents }
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "${}.{:02}", self.cents / 100, self.cents % 100)
}
}
impl Add for Money {
type Output = Money;
fn add(self, other: Money) -> Money {
Money { cents: self.cents + other.cents }
}
}
fn main() {
let coffee: Money = 350.into();
let cake = Money::from(405);
let total = coffee + cake;
println!("Кофе: {coffee}");
println!("Торт: {cake}");
println!("Итого: {total}");
}

Вывод:

Кофе: $3.50
Торт: $4.05
Итого: $7.55
▶ Попробуй сам
  • Debug ({:?}) — для разработчиков, его почти всегда выводят через derive. Display ({}) — для пользователей, его пишут вручную.
  • Clone копирует по запросу через .clone(). Copy копирует автоматически, но только простые типы.
  • PartialEq/Eq дают ==. PartialOrd/Ord дают < и сортировку, сравнивая поля сверху вниз.
  • Default создаёт начальное значение; ..Default::default() заполняет поля, которые ты не задал.
  • Реализуй From — и Into достанется даром. Оператор ? использует From, чтобы преобразовывать ошибки.
  • Реализуй std::ops::Add (и его родственников), чтобы + работал с твоими типами.

Хочешь глубже? В уроке Продвинутые трейты объясняются ассоциированные типы вроде type Output, параметры типа по умолчанию и то, как реализовать трейт для типа, который писал не ты.

Проверь себя

1. Какой трейт использует {} в println!?

2. Почему структура с полем String не может вывести Copy?

3. Как сравниваются две структуры, если PartialOrd выведен через derive?

4. Ты реализовал From<ParseIntError> for AppError. Что это позволяет делать ??

5. Какой трейт нужно реализовать, чтобы a + b работало для твоего типа?

Результат: 0 / 5