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

  1. Add a third type to the first program and implement the trait for it. Which method does it need?
  2. Give the trait a default method that returns the length of the title, and call it on both types.
  3. 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.

  1. 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.

  2. 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.

  3. What does &impl Summary in 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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: author

    The impl is incomplete, and the message names the missing method and the trait.

  10. 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