Трейты в Rust
Трейт (trait) описывает набор поведения, который тип может реализовать, — что-то вроде интерфейса в других языках. Так Rust говорит «этот тип умеет делать X». И именно на трейтах держатся ограничения, которые ты видел на странице Обобщения.
Объявляем трейт
Заголовок раздела «Объявляем трейт»trait Speak { fn speak(&self) -> String;}Смысл такой: любой тип, который реализует Speak, обязан предоставить метод speak, возвращающий String. Сам по себе трейт — просто обещание: он ничего не делает, пока какой-нибудь тип его не реализует.
Реализуем трейт
Заголовок раздела «Реализуем трейт»trait Speak { fn speak(&self) -> String;}
struct Dog;struct Cat;
impl Speak for Dog { fn speak(&self) -> String { String::from("Гав!") }}
impl Speak for Cat { fn speak(&self) -> String { String::from("Мяу!") }}
fn main() { let dog = Dog; let cat = Cat;
println!("{}", dog.speak()); println!("{}", cat.speak());}Вывод:
Гав!Мяу!Dog и Cat никак не связаны между собой, но оба реализуют Speak, поэтому у обоих можно вызвать .speak(). Это тот же приём, что и блоки impl на странице Структуры: impl Speak for Dog просто говорит «вот как Dog выполняет трейт Speak».
Трейты в параметрах функций
Заголовок раздела «Трейты в параметрах функций»А вот и главная польза: можно написать функцию, которая принимает любой тип, реализующий трейт, и ей неважно, какой именно:
trait Speak { fn speak(&self) -> String;}
struct Dog;impl Speak for Dog { fn speak(&self) -> String { String::from("Гав!") }}
fn announce(animal: &impl Speak) { println!("Животное говорит: {}", animal.speak());}
fn main() { let dog = Dog; announce(&dog);}animal: &impl Speak значит «любой тип, который реализует Speak, — какой именно, мне знать не нужно». Добавь тип Bird, который тоже реализует Speak, и announce заработает и с ним — без единого изменения.
Трейты, которыми ты уже пользовался
Заголовок раздела «Трейты, которыми ты уже пользовался»Помнишь #[derive(Debug)] со страницы Структуры? Debug — это тоже трейт, а derive просто сам генерирует реализацию, чтобы тебе не писать её вручную. Другие частые трейты, которые тебе встретятся: Clone (для .clone()), PartialEq (для ==) и PartialOrd (для < и >; ты видел его как ограничение на странице про обобщения).
Реализации по умолчанию
Заголовок раздела «Реализации по умолчанию»Трейт может дать методу тело по умолчанию. Типы, которые его реализуют, могут взять его как есть или переопределить:
trait Greet { fn name(&self) -> String;
fn greet(&self) -> String { format!("Привет, {}!", self.name()) }}Любому типу, реализующему Greet, достаточно определить name() — greet() достаётся даром, если только тип не захочет переопределить и его.
О трейтах можно рассказать гораздо больше, чем на этой странице. В уроке Популярные трейты разобраны стандартные трейты, которые нужны каждый день, а в уроке Продвинутые трейты — ассоциированные типы, супертрейты и другие инструменты, которые встречаются в больших проектах.
Практика
Заголовок раздела «Практика»1. Фигуры с площадью
Заголовок раздела «1. Фигуры с площадью»Объяви трейт Area с одним методом fn area(&self) -> f64. Реализуй его для Rectangle (с полями width и height) и для Square (с полем side). Выведи площадь прямоугольника 3 на 4 и квадрата со стороной 5. Подсказка: в каждом блоке impl Area for ... нужен только один метод, а его тело — одно умножение.
Показать решение
trait Area { fn area(&self) -> f64;}
struct Rectangle { width: f64, height: f64,}
struct Square { side: f64,}
impl Area for Rectangle { fn area(&self) -> f64 { self.width * self.height }}
impl Area for Square { fn area(&self) -> f64 { self.side * self.side }}
fn main() { let rect = Rectangle { width: 3.0, height: 4.0 }; let square = Square { side: 5.0 };
println!("Прямоугольник: {}", rect.area()); println!("Квадрат: {}", square.area());}Вывод:
Прямоугольник: 12Квадрат: 252. Одна функция для многих типов
Заголовок раздела «2. Одна функция для многих типов»Объяви трейт Describe с методом fn describe(&self) -> String. Реализуй его для Book (с полем title) и для Movie (с полями title и minutes). Затем напиши одну функцию fn show(item: &impl Describe), которая выводит описание, и вызови её с обоими. Подсказка: собирай каждую строку описания через format!.
Показать решение
trait Describe { fn describe(&self) -> String;}
struct Book { title: String,}
struct Movie { title: String, minutes: u32,}
impl Describe for Book { fn describe(&self) -> String { format!("Книга: {}", self.title) }}
impl Describe for Movie { fn describe(&self) -> String { format!("Фильм: {} ({} мин)", self.title, self.minutes) }}
fn show(item: &impl Describe) { println!("{}", item.describe());}
fn main() { let book = Book { title: String::from("Хоббит") }; let movie = Movie { title: String::from("Вверх"), minutes: 96 };
show(&book); show(&movie);}Вывод:
Книга: ХоббитФильм: Вверх (96 мин)3. Взять метод по умолчанию или переопределить
Заголовок раздела «3. Взять метод по умолчанию или переопределить»Возьми трейт Greet с этой страницы, с его методом greet() по умолчанию. Реализуй его для Person, который оставляет поведение по умолчанию, и для Pirate, который переопределяет greet() и говорит Йо-хо-хо, ...!. Подсказка: оба типа должны определить name(). Свой greet() пишет только Pirate.
Показать решение
trait Greet { fn name(&self) -> String;
fn greet(&self) -> String { format!("Привет, {}!", self.name()) }}
struct Person { name: String,}
struct Pirate { name: String,}
impl Greet for Person { fn name(&self) -> String { self.name.clone() }}
impl Greet for Pirate { fn name(&self) -> String { self.name.clone() }
fn greet(&self) -> String { format!("Йо-хо-хо, {}!", self.name()) }}
fn main() { let ana = Person { name: String::from("Аня") }; let jack = Pirate { name: String::from("Джек") };
println!("{}", ana.greet()); println!("{}", jack.greet());}Вывод:
Привет, Аня!Йо-хо-хо, Джек!- Трейт описывает поведение, которое тип может реализовать, — примерно как интерфейс.
- Реализацию даёт блок
impl ИмяТрейта for Тип. - Параметр типа
&impl ИмяТрейтапринимает любой тип, реализующий этот трейт. - Трейт может дать методам тела по умолчанию, которые типы берут как есть или переопределяют.
Debug,CloneиPartialEq— частые трейты стандартной библиотеки;#[derive(...)]реализует их автоматически.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Что описывает трейт?
2. Что означает тип параметра animal: &impl Speak?
3. Что на самом деле делает #[derive(Debug)]?
4. Что такое реализация по умолчанию в трейте?
5. Что из этого — трейт, а не структура или перечисление, судя по этой странице?
Результат: 0 / 5
Что почитать
Заголовок раздела «Что почитать»- Трейты: определение общего поведения — книга «Язык программирования Rust» (перевод на русский): официальная глава об объявлении трейтов, методах по умолчанию, параметрах
impl Traitи ограничениях трейтов. - Типажи — Rust на примерах (перевод на русский): запускаемый пример с трейтом, методом по умолчанию и реализацией.
- Атрибут Derive — Rust на примерах (перевод на русский): какие стандартные трейты
#[derive(...)]может реализовать за тебя, с примерами. - Продвинутые типажи — книга «Язык программирования Rust» (перевод на русский): следующий шаг — ассоциированные типы, супертрейты и паттерн newtype.
- Traits — The Rust Reference (на английском): точные правила объявления трейтов, когда нужны все подробности.