What a trait is¶
Level: 101 → 201 · for newcomers
One line: A trait names behaviour a type promises to have — the shared half that a struct deliberately does not hold.
trait Loud {
fn shout(&self) -> String; // no body: every implementor must write it
fn twice(&self) -> String { // a body: a default, taken unless replaced
format!("{} {}", self.shout(), self.shout())
}
}
struct Dog;
impl Loud for Dog {
fn shout(&self) -> String { "WOOF".to_string() }
}
fn main() {
println!("{}", Dog.twice()); // WOOF WOOF
}
Dog gained a method it does not define, from a trait that stores nothing. That is the whole idea, and everything below is a consequence of it.
Three kinds of item can go in a trait¶
Methods and functions, associated constants, and associated types. The first two are on this page; the third gets its own.
The semicolon decides whether an item is a demand or a gift. A function signature ending in ; is abstract — the compiler forces every implementor to write a body. A signature with a body is a default, and an implementor may take it or replace it. The same rule holds for associated constants: no = means the implementor must supply the value. Associated types are the exception with no middle ground — a trait may never define one, only demand it.
Self is the type that is implementing¶
Every trait has an implicit type parameter, Self, meaning "whichever type is implementing me". It is what lets one signature build many types:
trait Fresh {
fn fresh() -> Self; // an associated function: no `self`, so it is called on the TYPE
}
Star::fresh() returns a Star and Approval::fresh() returns an Approval, from that single line. And because the function has no self parameter, there is nothing to put a dot after — you name the type, exactly as with an inherent associated function.
What it is not¶
It is not a base class. A trait holds no fields, so there is nothing to inherit and no super. Two types implementing the same trait share a promise and not one byte of layout — the last section of the run below prints the sizes to prove it.
It is not quite an interface either. Java's interfaces cannot carry implementation; a Rust trait can, through default bodies. The nearer comparison is a Haskell type class, which is where the feature came from.
The error you will actually meet is E0277 — "the trait bound was not satisfied". One code covers four unrelated complaints, which is why when a struct refuses tells that half from the receiving end.
The verified output¶
Verified output of what_a_trait_is.rs — regenerated by tools/run_examples.py, never hand-typed.
1. The abstract method — the compiler forced both to write it
star.name() = STAR
approval.name() = Approval (approve at 3+)
2. The default body — taken by one, replaced by the other
star.describe() = STAR reads a 0-5 score ballot
approval.describe() = Approval (approve at 3+) — one bubble per candidate
3. The associated constant belongs to the TYPE, not the value
<Star as Method>::BALLOT = 0-5 score
<Approval as Method>::BALLOT = yes/no
4. `Self` = the implementing type, so one signature builds two
Star::fresh().name() = STAR
Approval::fresh().name() = Approval (approve at 3+)
5. A trait is a promise, not a payload: implementing it adds no bytes
size_of::<Star>() = 0
size_of::<Approval>() = 1
See also¶
implblocks — the inherent impl beside the trait impl: same syntax, different owner of the signatures- A trait must be in scope — the import that makes these methods appear, and the three ways to spell the call
- Returning a trait —
Box<dyn Trait>andimpl Trait, once one function has to answer with more than one type - What a struct is — the type with no behaviour in it, which is the gap this fills
CopyvsClone— a marker trait that changes what=means, beside one you call- Implementing
Iterator— required versus provided methods at scale: write one method, and seventy-five arrive
Po polsku¶
Po polsku mówi się na to cecha (trait) — powiedz tak raz, a potem używaj słowa trait, bo to słowo kluczowe i to ono pojawia się w każdym komunikacie kompilatora. Cecha nazywa zachowanie, które typ obiecuje mieć, i sama nie przechowuje niczego: Dog dostaje metodę twice(), której nie definiuje, od czegoś, co nie ma ani jednego pola. Widać to w wydruku powyżej — size_of::<Star>() to zero bajtów, mimo że Star implementuje całą cechę. Obietnica nic nie waży.
Reguła, którą warto wynieść z tej strony jako pierwszą, brzmi: decyduje średnik. Sygnatura zakończona średnikiem to żądanie — kompilator zmusi każdego implementującego do napisania ciała (odpowiednik metody abstrakcyjnej z Javy). Sygnatura z ciałem to podarunek: domyślna implementacja, którą można wziąć albo zastąpić. Ta sama zasada obowiązuje stałe powiązane, tyle że rolę średnika gra brak =. Typy powiązane są jedynym wyjątkiem bez stanu pośredniego — cecha może ich tylko zażądać, nigdy ich nie definiuje.
Na koniec dwie rzeczy, które polski czytelnik przynosi ze sobą z Javy i które trzeba odłożyć. Po pierwsze, cecha nie jest klasą bazową: nie ma pól, więc nie ma czego dziedziczyć ani do czego się odwoływać przez super, a dwa typy implementujące tę samą cechę nie dzielą ani jednego bajtu układu w pamięci. Do interfejsu też jej niezupełnie blisko — bliższym krewnym jest klasa typów (type class) z Haskella, skąd ta konstrukcja pochodzi. Po drugie, uważaj na parę Self i self: po polsku obie chce się przeczytać jako „ja”, a to dwie różne rzeczy — Self z wielkiej litery oznacza typ, który właśnie implementuje cechę (dlatego fn fresh() -> Self buduje raz Star, a raz Approval), natomiast self z małej to konkretna wartość. Funkcja bez self jest funkcją powiązaną: nie ma po czym postawić kropki, więc wywołuje się ją na typie. A błąd, na który trafisz najczęściej, to E0277 — jeden kod na cztery zupełnie różne skargi, więc czytaj nie tylko numer, ale i zdanie pod nim.
Szukaj po polsku: cechy w Ruscie · metoda domyślna · stała powiązana · rust trait vs interface · rust Self vs self · rust E0277 trait bound