Популярные трейты в Rust
На странице Трейты ты писал свои трейты. Но в стандартной библиотеке уже есть набор трейтов, которыми пользуется почти любая программа на Rust. Они отвечают на простые вопросы о типе: Можно ли его вывести на экран? Скопировать? Сравнить? Создать «пустое» значение?
Представь их как нашивки на форме. Каждая нашивка говорит: «этот тип умеет делать X». Многие из них достаются даром через #[derive(...)], другие пишутся вручную. На этой странице разобраны те, что встречаются чаще всего, а в конце есть таблица, к которой удобно возвращаться.
Debug: вывод для разработчика
Заголовок раздела «Debug: вывод для разработчика»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: вывод для пользователя
Заголовок раздела «Display: вывод для пользователя»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 HerbertThat text has 21 characterswrite! работает как println!, только пишет не на экран, а в форматтер f. Приятный бонус: как только у типа есть Display, ты бесплатно получаешь .to_string(). С Display ты ещё встретишься на странице Свои ошибки, где он даёт ошибкам понятное сообщение.
Clone и Copy
Заголовок раздела «Clone и Copy»Оба создают копию значения, но по-разному.
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 и Eq: сравнение через ==
Заголовок раздела «PartialEq и Eq: сравнение через ==»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: truea != c: trueА что такое Eq? Новых методов он не добавляет. Это обещание: «любое значение всегда равно самому себе». Звучит очевидно, но числа с плавающей точкой его нарушают: в Rust f64::NAN == f64::NAN — это false. Поэтому у f64 есть PartialEq, но нет Eq. Eq (вместе с Hash) нужен, чтобы использовать твой тип как ключ HashMap или хранить его в HashSet. Если в структуре нет полей с плавающей точкой, выводи оба.
PartialOrd и Ord: сравнение через < и >
Заголовок раздела «PartialOrd и Ord: сравнение через < и >»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: true1.2.01.10.32.0.0Раз поля сравниваются по порядку, порядок, в котором ты их пишешь, важен. Здесь самое важное поле — major, и для номеров версий это именно то, что нужно. У перечислений «меньше» считаются варианты, объявленные раньше. Как и с Eq, числа с плавающей точкой получают только PartialOrd, но не Ord; как их всё-таки отсортировать, показано на странице Сортировка.
Default: разумное начальное значение
Заголовок раздела «Default: разумное начальное значение»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 и Into: преобразование между типами
Заголовок раздела «From и Into: преобразование между типами»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 }))Подробнее эта идея развивается на странице Свои ошибки.
Перегрузка операторов через Add
Заголовок раздела «Перегрузка операторов через Add»В 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 expansion2 | 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 expansion2 | struct Product { name: String, price: f64 } | ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`Здесь компилятор сообщает, что f64 не реализует Eq. Оставь PartialEq и убери Eq. Или храни цену целыми копейками в u64 — для денег это ещё и надёжнее.
Практика
Заголовок раздела «Практика»1. Сортируем список дел
Заголовок раздела «1. Сортируем список дел»Создай структуру 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); }}Вывод:
Та же задача? trueTask { 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.55Debug({:?}) — для разработчиков, его почти всегда выводят через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
Что почитать
Заголовок раздела «Что почитать»- C — Выводимые трейты — книга «Язык программирования Rust» (перевод на русский): все трейты, которые можно
#[derive], что делает выведенная версия и когда она нужна. std::fmt— форматирование и Display (на английском): все параметры форматирования — ширина, выравнивание,{:#?}— и местоDisplayиDebugв этой системе.- From и Into — Rust на примерах (перевод на русский): короткие запускаемые примеры преобразования между своими типами.
std::ops— трейты операторов (на английском): полный список операторов, которые можно перегрузить, отAddдоIndex.std::cmp— трейты сравнения (на английском):PartialEq,Eq,PartialOrdиOrdподробно, с правилами, которые обещает каждый из них.