Трейты (Rust)
| Аналоги | интерфейсы (Java, Go), классы типов (Haskell) |
|---|---|
| Реализация | impl Трейт for Тип |
| Статическая диспетчеризация | обобщения с ограничениями (T Display), impl Trait |
| Динамическая диспетчеризация | dyn Trait через таблицу виртуальных методов |
| Автоматическая реализация | [derive(...)] |
| Ограничение | правило сироты (orphan rule) |
| Стандартные трейты | Debug, Display, Clone, Copy, From, Iterator, Send, Sync |
Трейт (англ. trait — «черта», «свойство»; в русских переводах также «типаж») — механизм Rust для описания общего поведения: набор методов, которые разные типы могут реализовать каждый по-своему. Трейты похожи на интерфейсы в Java и Go и на классы типов в Haskell. На них построено обобщённое программирование в Rust: функция может принимать «любой тип, который умеет выводиться на экран» или «любой итератор», а компилятор проверяет это на этапе сборки.[1]
Многие ключевые свойства языка выражены трейтами: копирование (Copy, Clone), вывод (Debug, Display), сравнение (PartialEq, Ord), итерация (Iterator), преобразования (From, Into) и потокобезопасность (Send, Sync).
Определение и реализация
trait Shape {
fn area(&self) -> f64; // обязательный метод
fn describe(&self) -> String { // метод с реализацией по умолчанию
format!("фигура площадью {:.2}", self.area())
}
}
struct Circle { r: f64 }
struct Rect { w: f64, h: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r }
}
impl Shape for Rect {
fn area(&self) -> f64 { self.w * self.h }
fn describe(&self) -> String { format!("прямоугольник {}×{}", self.w, self.h) }
}
В отличие от классического наследования, реализация трейта пишется отдельно от определения типа: можно реализовать свой трейт для чужих типов (например, для i32 или Vec<T>) и чужой трейт для своих.
Обобщения и ограничения
Трейты ограничивают параметры обобщённых функций — компилятор пропустит только подходящие типы:
use std::fmt::Display;
fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut max = items[0];
for &x in items { if x > max { max = x; } }
max
}
fn print_all(items: &[impl Display]) { // сокращённая запись ограничения
for x in items { println!("{x}"); }
}
fn total_area<S>(shapes: &[S]) -> f64
where
S: Shape,
{
shapes.iter().map(|s| s.area()).sum()
}
Обобщённый код проходит мономорфизацию: для каждого конкретного типа компилятор создаёт отдельную специализированную копию функции. Вызовы методов разрешаются при компиляции и могут встраиваться — это статическая диспетчеризация без накладных расходов во время выполнения, ценой большего размера бинарника.
Трейт-объекты
Когда в одной коллекции нужны значения разных типов, используют трейт-объекты dyn Trait. Значение хранится за указателем (Box, &, Arc), а метод выбирается во время выполнения через таблицу виртуальных методов — динамическая диспетчеризация:[2]
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { r: 1.0 }),
Box::new(Rect { w: 2.0, h: 3.0 }),
];
for s in &shapes {
println!("{}", s.describe());
}
Обобщения (T: Trait, impl Trait) |
Трейт-объекты (dyn Trait) |
|
|---|---|---|
| Выбор метода | при компиляции | во время выполнения |
| Скорость | максимальная, возможна оптимизация | косвенный вызов |
| Разнородные коллекции | нет | да |
| Размер кода | растёт с числом типов | одна копия |
Не каждый трейт можно сделать объектом: например, методы с обобщёнными параметрами или возвращающие Self этому мешают.[3]
Ассоциированные типы и стандартные трейты
Трейт может объявлять ассоциированный тип, который определяет реализация. Классический пример — Iterator:
struct Countdown(u32);
impl Iterator for Countdown {
type Item = u32; // ассоциированный тип
fn next(&mut self) -> Option<u32> {
if self.0 == 0 { None } else { self.0 -= 1; Some(self.0 + 1) }
}
}
let v: Vec<u32> = Countdown(3).filter(|x| x % 2 == 1).collect(); // [3, 1]
Реализовав один метод next, тип бесплатно получает десятки методов по умолчанию — map, filter, sum, collect.
Многие стандартные трейты можно реализовать автоматически атрибутом #[derive]:
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct UserId(u64);
Руководство по API для Rust рекомендует реализовывать для публичных типов стандартные трейты, где это имеет смысл, — так типы лучше сочетаются с остальной экосистемой.[6]
Правило сироты
Реализовать трейт для типа можно, только если трейт или тип определены в вашем крейте. Это правило сироты (orphan rule) не позволяет двум библиотекам дать конфликтующие реализации одного и того же трейта для одного типа. Обходной путь — обёртка-новый тип (newtype): struct Wrapper(Vec<String>);.
Трейты и асинхронность
С Rust 1.75 (декабрь 2023) в трейтах можно объявлять асинхронные методы async fn и методы, возвращающие impl Trait; до этого требовались обходные решения вроде крейта async-trait (см. async в Rust и Tokio).[5] Маркерные трейты Send и Sync сообщают компилятору, можно ли передавать значение между потоками и разделять его, — на этом основана защита от гонок данных (см. Владение и заимствование (Rust)). Больше примеров — в Rust By Example.[4]
См. также
- Rust (язык)
- Владение и заимствование (Rust)
- Обработка ошибок в Rust (Result, ?)
- async в Rust и Tokio
- Макросы Rust
Источники
- The Rust Programming Language — Traits: Defining Shared Behavior
- The Rust Programming Language — Using Trait Objects
- The Rust Reference — Traits
- Rust By Example — Traits
- Rust Blog — Announcing async fn and return-position impl Trait in traits (Rust 1.75)
- Rust API Guidelines — Interoperability (common traits)