Traits and default methods
In Rust a trait is a named set of methods that a type may implement. It is the closest thing Rust has to an interface, and it is also the mechanism behind operator overloading, formatting, iterators and the bounds on generic functions. One type may implement any number of traits, and a trait may be implemented for any number of types.
Coming from C++? Inheritance and traits writes a shape hierarchy with a virtual function and the same task with a trait, then passes the derived value by value in both languages: one loses the override, the other does not compile.
This lesson covers the declaration, the implementation, a default method and a function that accepts any implementor. Trait objects, which choose the implementation at run time, get the next lesson.
A trait is shared behaviour
The declaration lists the methods and their signatures, with no bodies. Each type that implements the trait writes the body for itself, and the compiler checks the signature. The two implementations below are for two unrelated structs.
The program calls the same method on an article and on a tweet and prints the two different strings. Nothing is shared between the types except the trait they implement.
trait Summary {
fn summarise(&self) -> String;
}
struct Article {
title: String,
}
struct Tweet {
user: String,
}
impl Summary for Article {
fn summarise(&self) -> String {
format!("article: {}", self.title)
}
}
impl Summary for Tweet {
fn summarise(&self) -> String {
format!("tweet by {}", self.user)
}
}
fn main() {
let article = Article { title: String::from("Rust") };
let tweet = Tweet { user: String::from("ada") };
println!("{}", article.summarise());
println!("{}", tweet.summarise());
}
rustc --edition 2024 trait.rs
./trait
article: Rust
tweet by ada
The trait is a contract, and the call site only needs the method name. The lesson on methods shows the impl block without a trait, which is the simpler form when there is only one type.
Default methods
A trait may provide a body, and an implementor inherits it unless it writes its own. The default method may call the required ones, which is how a trait offers a convenience method without forcing every type to repeat it.
The trait below requires title and provides summarise in terms of it. The article implements only the required method, and the call still works.
trait Summary {
fn title(&self) -> String;
fn summarise(&self) -> String {
format!("summary of {}", self.title())
}
}
struct Article {
title: String,
}
impl Summary for Article {
fn title(&self) -> String {
self.title.clone()
}
}
fn main() {
let article = Article { title: String::from("Rust") };
println!("{}", article.summarise());
}
rustc --edition 2024 default.rs
./default
summary of Rust
An implementor may override the default method, and the override wins for that type. The standard library uses this heavily: Iterator requires only next and provides dozens of adaptors.
A function with a bound
fn notify(item: &impl Summary) accepts a reference to any type that implements the trait. The caller does not name the trait at the call site, and the compiler checks the bound when the function is called.
The program passes an article and prints its title. The same function would accept a tweet without a change, because the only requirement is the trait.
trait Summary {
fn title(&self) -> String;
}
struct Article {
title: String,
}
impl Summary for Article {
fn title(&self) -> String {
self.title.clone()
}
}
fn notify(item: &impl Summary) {
println!("breaking: {}", item.title());
}
fn main() {
let article = Article { title: String::from("Rust") };
notify(&article);
}
rustc --edition 2024 bound.rs
./bound
breaking: Rust
The impl Trait form is sugar for a generic parameter with a bound, and the lesson on trait bounds writes the long form and explains when each is preferable.
Two implementations
A trait with one method and two implementations is the smallest polymorphic program. The call Square(3.0).area() picks the implementation from the receiver type, and no pointer or vtable is involved.
The program prints the area of a square and of a circle. The trait name does not appear at the call sites, and the methods are resolved when the program is compiled.
trait Area {
fn area(&self) -> f64;
}
struct Square(f64);
struct Circle(f64);
impl Area for Square {
fn area(&self) -> f64 {
self.0 * self.0
}
}
impl Area for Circle {
fn area(&self) -> f64 {
3.14159 * self.0 * self.0
}
}
fn main() {
println!("{:.2} {:.2}", Square(3.0).area(), Circle(1.0).area());
}
rustc --edition 2024 two_impls.rs
./two_impls
9.00 3.14
The implementation is chosen statically, which is why this form is called static dispatch. The next lesson shows the dynamic form, where the type is only known at run time and the call goes through a vtable.
A method left out
An implementation must provide every method that has no default. Leaving one out is E0046, and the message names the missing method and the trait it belongs to, so the fix is one method body.
The trait below requires two methods and the impl writes one. The compiler reports the missing author and points at the impl block.
trait Summary {
fn title(&self) -> String;
fn author(&self) -> String;
}
struct Article {
title: String,
}
impl Summary for Article {
fn title(&self) -> String {
self.title.clone()
}
}
fn main() {
let article = Article { title: String::from("Rust") };
println!("{}", article.title());
}
rustc --edition 2024 missing_method_err.rs
error[E0046]: not all trait items implemented, missing: `author`
--> missing_method_err.rs:10:1
|
3 | fn author(&self) -> String;
| --------------------------- `author` from trait
...
10 | impl Summary for Article {
| ^^^^^^^^^^^^^^^^^^^^^^^^ missing `author` in implementation
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0046`.
A default body would make the method optional, which is the design choice the previous section shows. The error is the compiler asking which of the two the programmer meant.
Exercises
- Add a third type to the first program and implement the trait for it. Which method does it need?
- Give the trait a default method that returns the length of the title, and call it on both types.
- Write a function that accepts &impl Area and prints the area of anything that implements it.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is a trait?
Answer
A named set of methods that a type can implement
A trait declares behaviour, and each implementing type supplies the bodies.
-
What does a default method in a trait do?
Answer
Provides a body that implementors inherit unless they override it
The default body is used when the impl does not provide its own.
-
What does
&impl Summaryin a parameter mean?Answer
A reference to any type that implements Summary
The parameter is generic over the implementors, with the trait as the bound.
-
What does E0046 report?
Answer
A trait method that the implementation does not provide
The compiler lists the missing trait items and points at the impl block.
-
What does this program print?
trait Summary { fn summarise(&self) -> String; } struct Article { title: String, } impl Summary for Article { fn summarise(&self) -> String { format!("article: {}", self.title) } } fn main() { let article = Article { title: String::from("Rust") }; println!("{}", article.summarise()); }Answer
article: Rust
The implementation formats the title into the summary, and the call prints it.
-
What does this program print?
trait Summary { fn title(&self) -> String; fn summarise(&self) -> String { format!("summary of {}", self.title()) } } struct Article { title: String, } impl Summary for Article { fn title(&self) -> String { self.title.clone() } } fn main() { let article = Article { title: String::from("Rust") }; println!("{}", article.summarise()); }Answer
summary of Rust
The default body calls the implemented title method, which returns the field.
-
What does this program print?
trait Area { fn area(&self) -> f64; } struct Square(f64); impl Area for Square { fn area(&self) -> f64 { self.0 * self.0 } } fn report(item: &impl Area) { println!("{:.2}", item.area()); } fn main() { report(&Square(3.0)); }Answer
9.00
The bound accepts the square, and the format prints the area with two decimals.
-
What does this program print?
trait Area { fn area(&self) -> f64; } struct Square(f64); struct Circle(f64); impl Area for Square { fn area(&self) -> f64 { self.0 * self.0 } } impl Area for Circle { fn area(&self) -> f64 { 3.14159 * self.0 * self.0 } } fn main() { println!("{:.2} {:.2}", Square(3.0).area(), Circle(1.0).area()); }Answer
9.00 3.14
Each type uses its own implementation, and the two areas print in call order.
-
What is the first line of the compiler output for this program?
trait Summary { fn title(&self) -> String; fn author(&self) -> String; } struct Article { title: String, } impl Summary for Article { fn title(&self) -> String { self.title.clone() } } fn main() { let article = Article { title: String::from("Rust") }; println!("{}", article.title()); }Answer
error[E0046]: not all trait items implemented, missing:
authorThe impl is incomplete, and the message names the missing method and the trait.
-
What does this program print?
trait Name { fn name(&self) -> String; } struct A; struct B; impl Name for A { fn name(&self) -> String { String::from("A") } } impl Name for B { fn name(&self) -> String { String::from("B") } } fn main() { let values: Vec<&dyn Name> = vec![&A, &B]; for value in values { println!("{}", value.name()); } }Answer
A / B
The vector holds two trait objects, and each call runs its own implementation.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev