A trait is Rust's tool for polymorphism. It is similar to an "interface" in other languages, but it can have default methods and supports both compile-time monomorphization and runtime dynamic dispatch. Examples in the standard library include Iterator, Display, Debug, From, all of which are traits and are key vocabulary in library development.
Learning Objectives
- Define a trait with the
traitkeyword and implement it for a type withimpl Trait for Type. - Define a default method to provide common behavior.
- Implement runtime polymorphism using
&dyn Trait/Box<dyn Trait>trait objects. - Frequently used traits (
Debug,Clone,PartialEq) are automatically implemented withderive.
Key Concepts
1) Defining and Implementing Traits
trait Summary {
fn title(&self) -> String; //Signature only
fn summarize(&self) -> String { //Basic implementation
format!("({}) Summary", self.title())
}
}
impl Summary for Article {
fn title(&self) -> String { self.headline.clone() }
}
Implementers must provide the methods that only have a signature. Default methods can be left as-is or overridden.
2) Trait Parameters
fn print(item: &impl Summary) { /* ... */ } // impl Trait
fn print<T: Summary>(item: &T) { /* ... */ } // Generic + bound
fn print<T>(item: &T) where T: Summary { /* ... */ } // where clause
The three notations have the same meaning. Choose based on readability.
3) Trait Object — Runtime Polymorphism
Used when you want to put multiple types implementing the same trait into a single collection or when the return type needs to be determined dynamically.
fn area(s: &dyn Shape) -> f64 { s.area() }
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle{...}), Box::new(Square{...})];
&dyn Trait— reference-type trait objectBox<dyn Trait>— heap-allocated owning trait object
(compared to static dispatch (impl Trait), there is a slight cost when calling because it goes through the virtual function table, but the code becomes more flexible.)
4) Automatic Implementation of derive
| Trait | Effect |
|---|---|
Debug |
Prints {:?} |
Clone |
Deep copy of .clone() |
Copy |
Automatic bit copy (Clone also needed) |
PartialEq |
Comparison of == / != |
Eq, Hash, Ord |
Keys for sorted or hash containers |
Default |
Default for T::default() |
5) When a Trait is Blocked — orphan rule
A trait you define can be implemented for any type, and any trait can be implemented for a type you define. Both external is not allowed.
Combinations like String with Display (external+external) cannot be implemented directly.
Examples
Example 1 — ex01_summary.rs: Summary trait and default methods
//Example 1 of Chapter 14: Trait Definition, Implementation, and Default Methods
//
//A trait is similar to an "interface" in other languages, but it can have default methods.
//It can provide common behavior without forcing partial implementation.
trait Summary {
//Methods with empty implementations — must be defined by the implementer
fn title(&self) -> String;
//Default methods — this body is used if the implementer does not override
fn summarize(&self) -> String {
format!("A summary of ({})", self.title())
}
}
struct Article {
headline: String,
body: String,
}
impl Summary for Article {
fn title(&self) -> String {
self.headline.clone()
}
//summarize uses the default implementation
//If you want to see the body as well, you can directly override
//fn summarize(&self) -> String {
// format!("[{}] {} ...", self.headline, &self.body[..self.body.len().min(20)])
//}
}
struct Tweet {
user: String,
text: String,
}
impl Summary for Tweet {
fn title(&self) -> String {
format!("@{}", self.user)
}
fn summarize(&self) -> String {
format!("@{}: {}", self.user, self.text)
}
}
fn main() {
let a = Article {
headline: String::from("Rust 1.0 release"),
body: String::from("Rust has reached a stable version."),
};
let t = Tweet {
user: String::from("rustlang"),
text: String::from("hello from rustlang"),
};
println!("{}", a.summarize());
println!("{}", t.summarize());
}
Example 2 — ex02_shape.rs: trait object
//Example 14-2: trait object (`dyn Shape`) — Runtime polymorphism
//
//To put different types that implement a trait into the same collection and call the same method,
//use a "trait object." Usually `Box<dyn Trait>` or `&dyn Trait`.
use std::f64::consts::PI;
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &'static str;
}
struct Circle { r: f64 }
struct Square { side: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { PI * self.r * self.r }
fn name(&self) -> &'static str { "Circle" }
}
impl Shape for Square {
fn area(&self) -> f64 { self.side * self.side }
fn name(&self) -> &'static str { "Square" }
}
fn print_area(s: &dyn Shape) {
println!("{}: area = {:.2}", s.name(), s.area());
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { r: 3.0 }),
Box::new(Square { side: 4.0 }),
];
for s in &shapes {
print_area(s.as_ref());
}
}
Example 3 — ex03_derive.rs: derive macro
//Example 3 of Chapter 14: Automatic Implementation of Common Traits with the derive Macro
//
//Commonly used traits can be automatically implemented with `#[derive(...)]`.
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
//Clone — Deep copy
let p2 = p1.clone();
//PartialEq — Can use == / != for comparison
println!("p1 == p2 ? {}", p1 == p2);
//Debug — Can be printed with {:?} / {:#?}
println!("p1 = {:?}", p1);
println!("p1 = {:#?}", p1); //Indentation format
}
Common Mistakes
Q. What is the difference between impl Trait and dyn Trait?
A. impl Trait is monomorphized at compile time, so the call cost is 0, but you can only use one concrete type per function. dyn Trait uses runtime dynamic dispatch, so you can mix multiple types in the same place, but there is a virtual function table lookup cost when calling.
Q. Why doesn't Vec<dyn Trait> work?
A. A trait object is a dynamically sized type whose size cannot be known at compile time, so it cannot go directly into Vec elements. You need to add one level of indirection and use it like Vec<Box<dyn Trait>>.
Q. I only used derive(Clone), but calling clone() doesn't work.
A. Some types of fields inside the structure may not implement Clone. All fields must be Clone for automatic derive to pass.
Q. Can I return Self in a trait?
A. It is possible. However, to make it a trait object (because of object safety rules), you cannot have a method that returns Self. At the introductory level, it is safer to handle it with static dispatch (impl Trait).
Summary
- The basic pattern is defining
trait+ implementingimpl Trait for Type. - Provide common behavior with default methods, and implementations can override when needed.
- Express runtime polymorphism with
&dyn Trait/Box<dyn Trait>. - Common traits are automatically implemented with
#[derive(...)]—Debug,Clone,PartialEqare frequently used.
Try it yourself
Solve the assignment below. The reference solution is shown below.