Traits
Prerequisites: Generics
A trait describes behavior that several types can share: a set of method
signatures a type promises to provide. Traits are similar to interfaces in
other languages. They are also what trait bounds (T: PartialOrd) refer to.
Syntax
trait Summary {
fn author(&self) -> String; // required: every implementor must write it
fn summarize(&self) -> String { // default: implementors get this for free
format!("(Read more from {}...)", self.author())
}
}
struct Post {
user: String,
}
impl Summary for Post { // implement the trait for a type
fn author(&self) -> String {
format!("@{}", self.user)
}
}
Ways to accept "any type that implements Summary":
fn notify(item: &impl Summary) { /* ... */ } // impl Trait in argument position
fn notify_generic<T: Summary>(item: &T) { /* ... */ } // the same thing, as a trait bound
fn compare<T, U>(a: &T, b: &U) // a where clause, for longer bounds
where
T: Summary + Clone,
U: Summary,
{ /* ... */ }
Example
trait Summary {
fn author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.author())
}
}
struct Article {
title: String,
writer: String,
}
struct Post {
user: String,
text: String,
}
impl Summary for Article {
fn author(&self) -> String {
self.writer.clone()
}
// Override the default method.
fn summarize(&self) -> String {
format!("{}, by {}", self.title, self.author())
}
}
impl Summary for Post {
fn author(&self) -> String {
format!("@{}", self.user)
}
// Uses the default summarize().
}
fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
fn longest_summary<A, B>(a: &A, b: &B) -> String
where
A: Summary,
B: Summary,
{
let (sa, sb) = (a.summarize(), b.summarize());
if sa.len() >= sb.len() { sa } else { sb }
}
// impl Trait in return position: "returns some type that implements Summary".
fn sample_post() -> impl Summary {
Post { user: String::from("rustlang"), text: String::from("Rust 2024 is out") }
}
fn main() {
let article = Article {
title: String::from("Traits explained"),
writer: String::from("Ada"),
};
let post = Post { user: String::from("ferris"), text: String::from("hello!") };
notify(&article);
notify(&post);
println!("{} says: {}", post.author(), post.text);
println!("Longest: {}", longest_summary(&article, &post));
println!("{}", sample_post().summarize());
}
Expected output
Breaking news! Traits explained, by Ada
Breaking news! (Read more from @ferris...)
@ferris says: hello!
Longest: (Read more from @ferris...)
(Read more from @rustlang...)
Trait objects
impl Summary and T: Summary are resolved at compile time, so every call
uses one concrete type. To keep different types in one collection, use a
trait object, dyn Summary, behind a pointer such as Box or &. Method
calls then go through a small lookup table at runtime:
let items: Vec<Box<dyn Summary>> = vec![Box::new(article), Box::new(post)];
for item in &items {
println!("{}", item.summarize());
}
Rules to know
- You can implement a trait for a type only if the trait or the type is
defined in your crate (the "orphan rule"). You can implement your
SummaryforVec<i32>, or the standardDisplayfor yourArticle, but not the standardDisplayfor the standardVec. - A trait's methods are only callable when the trait is in scope. If a method
"doesn't exist", check for a missing
use some_crate::SomeTrait;.
Common mistakes
- Leaving out a required method:
error[E0046]: not all trait items implemented, missing: `author`. - Returning two different types from a function declared
-> impl Summary, for example anArticlein one branch and aPostin the other:error[E0308]: `if` and `else` have incompatible types.impl Traitstill means one concrete type; returnBox<dyn Summary>if it really varies. - Writing
Vec<dyn Summary>without a pointer. Trait objects have no fixed size:error[E0277]: the size for values of type `dyn Summary` cannot be known at compilation time. - Calling a trait method without importing the trait:
error[E0599]: no method named `summarize` found for struct `Article` in the current scope, with a hint to add auseline.
Practice
Define a trait Area with a required method area(&self) -> f64 and a default
method describe(&self) -> String returning "area = <area>" with two decimal
places. Implement it for Circle { radius: f64 } and Rect { w: f64, h: f64 },
then write fn total_area(shapes: &[Box<dyn Area>]) -> f64 and print the
description of a circle with radius 1 and the total area of that circle plus
a 2 × 3 rectangle.
Show solution
trait Area {
fn area(&self) -> f64;
fn describe(&self) -> String {
format!("area = {:.2}", self.area())
}
}
struct Circle {
radius: f64,
}
struct Rect {
w: f64,
h: f64,
}
impl Area for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl Area for Rect {
fn area(&self) -> f64 {
self.w * self.h
}
}
fn total_area(shapes: &[Box<dyn Area>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn main() {
let circle = Circle { radius: 1.0 };
println!("{}", circle.describe());
let shapes: Vec<Box<dyn Area>> = vec![Box::new(circle), Box::new(Rect { w: 2.0, h: 3.0 })];
println!("total = {:.2}", total_area(&shapes));
}
area = 3.14
total = 9.14