Interfaces and traits
An interface is a pure contract: a list of methods that a type promises to provide, with no data attached. It describes what a type can do, not what it is. A Truck, a Robot and a Drone are different kinds of thing, but all of them can move, so a "movable" contract fits them all.
Rust has no interface keyword. Its interfaces are traits. A trait lists method signatures, and a type opts in with a separate block, impl TraitName for TypeName. Three things set traits apart:
- Data, contract and behaviour are separate. The struct holds fields, the trait states the contract, and the
implblock supplies the behaviour. - A type can implement many traits. Nothing limits how many
impl Trait for Typeblocks one type has. - Implementing is explicit. A type does not satisfy a trait just because it has methods with matching names. If
Truckhas noimpl Movable for Truck, passing it where aMovableis required fails witherror[E0277]: the trait bound `Truck: Movable` is not satisfied.
Step by step
The lesson builds the whole program from scratch.
- Define the trait
Speakerwith one method,fn speak(&self);. It has no body, so every implementor must provide one. - Declare
struct Dog;, a unit struct with no fields. All the dog has to offer is behaviour. - Write
impl Speaker for Dog. The trait comes first and the type second. The block must define every method the trait declares, or the code does not compile. - Define
speakinside it with the same signature as in the trait, taking&self, and printWoofwith theprintln!macro. The&selfborrows the dog read-only. - Add
main_function, a function with no parameters that uses the contract. - Store the dog as a trait object. Declare
rexwith the typeBox<dyn Speaker>, create it withBox::new(Dog), and callrex.speak(). The variable's type says only "something that can speak", and the real implementation is chosen at run time.
The solution in Rust
trait Speaker {
fn speak(&self);
}
struct Dog;
impl Speaker for Dog {
fn speak(&self) {
println!("Woof");
}
}
fn main_function() {
let rex: Box<dyn Speaker> = Box::new(Dog);
rex.speak();
}
The output is:
Woof
dyn Speaker has no fixed size, because any type may implement the trait, so it lives behind a pointer. Box provides that pointer by putting the value on the heap. Another type implementing Speaker could replace Dog in that line, and the call below it would not change.
Two ways to use a trait
The trait object above uses dynamic dispatch. The other way is a generic bound, where the compiler builds a specialised copy of the function for each concrete type:
trait Movable { fn go(&self); }
trait Speaker { fn speak(&self); }
struct Robot;
struct Truck;
impl Movable for Robot { fn go(&self) { println!("Robot rolls"); } }
impl Speaker for Robot { fn speak(&self) { println!("Beep"); } }
impl Movable for Truck { fn go(&self) { println!("Truck drives"); } }
fn start(m: &impl Movable) { m.go(); }
fn both<T: Movable + Speaker>(t: &T) {
t.go();
t.speak();
}
fn main() {
start(&Truck);
both(&Robot);
let fleet: Vec<Box<dyn Movable>> = vec![Box::new(Robot), Box::new(Truck)];
for m in &fleet {
m.go();
}
}
It prints:
Truck drives
Robot rolls
Beep
Robot rolls
Truck drives
Robot implements two traits, and both requires both with Movable + Speaker. Truck implements only Movable, so both(&Truck) would not compile. The fleet vector mixes types behind Box<dyn Movable>.
Choosing between them
- Use a generic bound or
impl Traitwhen the function works with one concrete type per call. There is no run-time lookup. - Use
dyn Traitwhen one variable or collection must hold different types, as infleet.
Interface or abstract class
| Abstract class | Rust trait | |
|---|---|---|
| Holds fields | Yes | No |
| Method bodies | Some | Optional defaults |
| Several at once | Usually one parent | Any number per type |
| Models | "is a" | "can do" |
Common mistakes
- Writing the method in a plain
impl Dogblock. It then belongs toDogbut does not satisfySpeaker. - Leaving out a required method. The compiler reports that not all trait items are implemented.
- Using a bare trait as a type. In edition 2021 a type must be written
Box<dyn Speaker>or&dyn Speaker;Speakeralone is an error. - Forgetting
Box::new. ADogvalue does not convert toBox<dyn Speaker>by itself.
Key takeaways
- Traits are Rust's interfaces: a contract of method signatures, implemented explicitly per type.
- A type can implement any number of traits.
- Generic bounds give static dispatch, and
Box<dyn Trait>gives dynamic dispatch over mixed types.
