общее поведение без наследования классовshared behavior without class inheritance
Trait определяет набор методов, которые может реализовать тип, — механизм Rust для общего поведения между иначе не связанными типами, примерно аналогичный интерфейсу в Java/C# или абстрактному базовому классу в C++, но разрешаемый преимущественно на этапе компиляции (статическая диспетчеризация через мономорфизацию), а не через vtable по умолчанию.A trait defines a set of methods a type can implement — Rust's mechanism for shared behavior across otherwise-unrelated types, roughly analogous to an interface in Java/C# or an abstract base class in C++, but resolved primarily at compile time (static dispatch via monomorphization) rather than through a vtable by default.
Trait определяет набор методов, которые тип может реализовать, — механизм Rust для общего поведения между иначе не связанными типами, примерно аналогичный интерфейсу в Java/C# или абстрактному базовому классу в C++ с чисто виртуальными методами, но разрешаемый преимущественно на этапе компиляции (статическая диспетчеризация через мономорфизацию, как шаблоны/концепты C++), а не через vtable по умолчанию. Trait может предоставлять реализации методов по умолчанию, которые реализующие типы наследуют бесплатно, если не переопределяют их. Trait реализуется для типа через impl TraitName for TypeName { ... } и может требоваться как ограничение (bound) для дженерик-функции: fn process<T: Display>(item: T).A trait defines a set of methods a type can implement — Rust's mechanism for shared behavior across otherwise-unrelated types, roughly analogous to an interface in Java/C# or a C++ abstract base class with pure virtual methods, but resolved primarily at compile time (static dispatch via monomorphization, like C++ templates/concepts) rather than through a vtable by default. A trait can provide default method implementations that implementing types inherit for free unless they override them. Traits are implemented for a type via impl TraitName for TypeName { ... }, and can be required as bounds on generic functions: fn process<T: Display>(item: T).
Отказ от наследования классов — осознанное архитектурное решение Rust:Rejecting class inheritance was a deliberate architectural decision in Rust:
В Rust намеренно нет наследования классов (никакого class Derived : public Base) — это осознанный выбор, чтобы избежать хорошо известных ловушек глубоких иерархий наследования: проблема хрупкого базового класса, неоднозначность ромбовидного наследования, тесная связанность деталей реализации базового и производного классов.Rust deliberately has no class inheritance at all (no class Derived : public Base) — a considered design choice to avoid the well-known pitfalls of deep inheritance hierarchies: the fragile base class problem, diamond inheritance ambiguity, tight coupling between base and derived implementation details.
Traits дают полезную часть интерфейсов/полиморфизма (общие контракты поведения, дженерик-код, работающий над "чем угодно, способным на X"), но композируются через реализацию (тип может реализовать любое число несвязанных трейтов), а не через единую линейную цепочку наследования — это ближе по духу к интерфейсам Go или тайпклассам Haskell, чем к иерархиям классов Java/C++.Traits give you the useful part of interfaces/polymorphism (shared behavior contracts, generic code working over "anything that can do X") while composing via implementation (a type can implement any number of unrelated traits) rather than via a single linear inheritance chain — closer in spirit to Go's interfaces or Haskell's typeclasses than to Java/C++ class hierarchies.
Такая модель напрямую включает диспетчеризацию по умолчанию как статическую, чего виртуальные функции C++ не дают по умолчанию — виртуальный вызов всегда идёт через vtable-индирекцию, если только оптимизатор не девиртуализирует его.This model directly enables Rust's default of static dispatch, which C++ virtual functions don't provide by default — a virtual call always goes through a vtable indirection unless the optimizer devirtualizes it.
Определение trait'а с одним обязательным и одним default-методом, реализация для двух несвязанных типов, затем дженерик-функция с ограничением по trait'у:Defining a trait with one required and one default method, implementing it for two unrelated types, then a generic function bounded by the trait:
trait Summary {
fn summarize(&self) -> String; // обязательный метод// required method
fn summarize_default(&self) -> String { // метод по умолчанию// default method
format!("(Read more from {}...)", self.summarize())
}
}
struct Article { headline: String }
struct Tweet { handle: String }
impl Summary for Article {
fn summarize(&self) -> String { self.headline.clone() }
}
impl Summary for Tweet {
fn summarize(&self) -> String { format!("@{}", self.handle) }
}
fn notify<T: Summary>(item: &T) { println!("{}", item.summarize()); }
Эквивалентный сахар в позиции аргумента — impl Trait, компилируется в то же самое:The equivalent argument-position sugar — impl Trait — compiles to the same thing:
fn notify(item: &impl Summary) { println!("{}", item.summarize()); }
Article (новостная статья) и Tweet (пост в соцсети) — совершенно не связаны иерархией, но обе реализуют Summary, что позволяет одной функции репортинга работать с обеими.Article and Tweet are entirely unrelated by any hierarchy, but both implement Summary, which lets a single reporting function work over both.
fn report<T: Summary>(items: &[T]) {
for item in items {
println!("{}", item.summarize());
}
}
// report(&articles); report(&tweets); — одна функция, два несвязанных типа// report(&articles); report(&tweets); — one function, two unrelated types
greet() реализован один раз в самом trait'е и вызывает абстрактный name() — каждый реализующий тип пишет только name(), а не копирует одну и ту же non-virtual "template method" логику в каждый производный класс, как в C++.greet() is implemented once in the trait itself and calls the abstract name() — each implementor writes only name(), instead of copying the same non-virtual "template method" logic into every derived class as in C++.
trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String { format!("Hello, {}!", self.name()) }
}
struct Guest;
impl Greet for Guest {
fn name(&self) -> String { String::from("Guest") } // greet() наследуется бесплатно// greet() is inherited for free
}
Перегрузка операторов в Rust — "просто" реализация конкретного стандартного trait'а, а не особый синтаксис компилятора.Operator overloading in Rust is "just" implementing a specific standard-library trait, not special compiler syntax.
use std::ops::Add;
#[derive(Clone, Copy)]
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 }
}
}
// p1 + p2 теперь допустимый синтаксис благодаря impl Add// p1 + p2 is now valid syntax, thanks to impl Add
Комбинация нескольких trait bound'ов через +, и то же самое через where для читаемости, когда ограничений становится много. Ближайший современный C++-аналог для выражения такого же compile-time-проверяемого ограничения — концепты (concepts-language.html).Combining several trait bounds via +, and the same expressed via where for readability when bounds get numerous. The closest modern C++ analogue for expressing the same kind of compile-time-checked constraint is concepts (concepts-language.html).
fn process<T: Display + Clone>(item: T) { println!("{}", item); }
fn process2<T>(item: T)
where
T: Display + Clone,
{
println!("{}", item);
}
Реализовать trait для типа можно, только если trait ИЛИ тип определены в вашем собственном крейте, — это предотвращает конфликтующие реализации в экосистеме (два крейта, каждый реализующий Display для String, дали бы неразрешимый конфликт), но означает, что иногда нужен локальный newtype-обёртка (см. паттерн newtype в structs-enums.html) чисто ради обхода этого правила, что удивляет новичков, ожидающих большей гибкости.You can only implement a trait for a type if either the trait or the type is defined in your own crate — this prevents conflicting implementations across the ecosystem (two crates both implementing Display for String would be an unresolvable conflict), but it does mean you sometimes need a local newtype wrapper (see the newtype pattern in structs-enums.html) purely to work around this rule, which surprises newcomers expecting more flexibility.
Дженерик-функция с ограничением по trait'у (fn f<T: Summary>(x: T)) статически диспетчеризуется и мономорфизируется (отдельная скомпилированная копия на каждый конкретный использованный тип — тот же компромисс, что у шаблонов C++, раздутие кода ради нулевой цены вызова), тогда как dyn Trait (см. trait-objects.html) диспетчеризуется динамически через vtable (одна скомпилированная копия, но рантайм-индирекция на каждый вызов). Путаница, когда что применимо, — частый источник непонимания.A generic function bounded by a trait (fn f<T: Summary>(x: T)) is statically dispatched and monomorphized (a separate compiled copy per concrete type used, the same tradeoff as C++ templates — code bloat for zero call overhead), whereas dyn Trait (see trait-objects.html) is dynamically dispatched via a vtable (one compiled copy, but a runtime indirection per call). Mixing up which applies when is a common point of confusion.
Default-методы можно переопределить, но у Rust нет аналога C++ final, чтобы это запретить, и нет аналога явного вызова "версии базового класса" (Base::method() в C++) изнутри переопределения — трейт-модель Rust не имеет этого просто потому, что нет цепочки наследования, "вверх" по которой можно было бы вызывать. Это иногда требует перестройки кода, который в ООП-языке полагался бы на этот паттерн.Default methods can be overridden, but Rust has no equivalent of C++'s final to prevent it, nor any equivalent of explicitly calling "the base class's version" (C++'s Base::method()) from an override — Rust's trait model doesn't have this because there's no inheritance chain to call "up" through. This occasionally requires restructuring code that, in an OOP language, would rely on that pattern.