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

Трейты в 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() достаётся даром, если только тип не захочет переопределить и его.

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

Объяви трейт 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
Квадрат: 25
▶ Попробуй сам

Объяви трейт 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