On this page
Traits: abstraction and shared behavior
Traits: shared behavior
A trait defines a set of methods that a type can implement. They are similar to interfaces in other languages, but more powerful: they allow default implementations, can have bounds on generic types, and are used extensively throughout the standard library.
Defining and implementing traits
trait Greetable {
// Method the type MUST implement
fn greet(&self) -> String;
// Method with default implementation
fn farewell(&self) -> String {
format!("Goodbye, {}!", self.name())
}
// Another abstract method
fn name(&self) -> &str;
}
struct Person {
name: String,
language: String,
}
struct Robot {
id: u32,
model: String,
}
impl Greetable for Person {
fn greet(&self) -> String {
match self.language.as_str() {
"es" => format!("Hola! I'm {}", self.name),
"en" => format!("Hello! I'm {}", self.name),
_ => format!("Hi! I'm {}", self.name),
}
}
fn name(&self) -> &str {
&self.name
}
}
impl Greetable for Robot {
fn greet(&self) -> String {
format!("ROBOT-{} ({}) ONLINE. PROCESSING...", self.id, self.model)
}
fn name(&self) -> &str {
&self.model
}
// Override the default method
fn farewell(&self) -> String {
format!("ROBOT-{} OFFLINE.", self.id)
}
}
fn main() {
let person = Person {
name: String::from("Alice"),
language: String::from("en"),
};
let robot = Robot { id: 42, model: String::from("T-800") };
println!("{}", person.greet());
println!("{}", person.farewell()); // Default method
println!("{}", robot.greet());
println!("{}", robot.farewell()); // Overridden implementation
}Traits as parameters
There are two syntaxes for using traits in function parameters:
trait Area {
fn area(&self) -> f64;
}
struct Square { side: f64 }
struct Circle { radius: f64 }
impl Area for Square {
fn area(&self) -> f64 { self.side * self.side }
}
impl Area for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
// Syntax 1: impl Trait (syntactic sugar, static dispatch)
fn print_area(shape: &impl Area) {
println!("Area: {:.4}", shape.area());
}
// Syntax 2: explicit trait bound (more flexible with generics)
fn print_area_generic<T: Area>(shape: &T) {
println!("Area: {:.4}", shape.area());
}
// With where clause (more readable with multiple bounds)
fn compare_areas<T>(shape1: &T, shape2: &T) -> bool
where
T: Area,
{
shape1.area() > shape2.area()
}
fn main() {
let s = Square { side: 4.0 };
let c = Circle { radius: 3.0 };
print_area(&s);
print_area_generic(&c);
println!("Does square have more area? {}", compare_areas(&s, &c));
}Multiple trait bounds
use std::fmt::{Debug, Display};
fn print_debug_and_display<T>(value: &T)
where
T: Debug + Display,
{
println!("Debug: {:?}", value);
println!("Display: {}", value);
}
fn compare_and_show<T>(a: T, b: T) -> T
where
T: PartialOrd + Display + Clone,
{
if a > b {
println!("{a} > {b}");
a
} else {
println!("{a} <= {b}");
b.clone()
}
}
fn main() {
print_debug_and_display(&42);
print_debug_and_display(&"hello");
let larger = compare_and_show(10, 20);
println!("Larger: {larger}");
}Supertraits
A trait can require that a type also implement another trait (supertrait):
use std::fmt;
// Animal requires that the type implement fmt::Display
trait Animal: fmt::Display {
fn sound(&self) -> &str;
fn legs(&self) -> u8;
fn describe(&self) -> String {
format!("{} makes '{}' and has {} legs", self, self.sound(), self.legs())
}
}
struct Dog { name: String }
struct Cat { name: String }
impl fmt::Display for Dog {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Dog({})", self.name)
}
}
impl fmt::Display for Cat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Cat({})", self.name)
}
}
impl Animal for Dog {
fn sound(&self) -> &str { "woof" }
fn legs(&self) -> u8 { 4 }
}
impl Animal for Cat {
fn sound(&self) -> &str { "meow" }
fn legs(&self) -> u8 { 4 }
}
fn main() {
let dog = Dog { name: String::from("Rex") };
let cat = Cat { name: String::from("Whiskers") };
println!("{}", dog.describe());
println!("{}", cat.describe());
}Implementing Display and Debug
The Display and Debug traits are fundamental for making your types printable:
use std::fmt;
struct Temperature {
celsius: f64,
}
// Display: for user-facing output
impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.1}°C", self.celsius)
}
}
// Debug: for debugging output
impl fmt::Debug for Temperature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Temperature")
.field("celsius", &self.celsius)
.field("fahrenheit", &(self.celsius * 1.8 + 32.0))
.finish()
}
}
fn main() {
let t = Temperature { celsius: 100.0 };
println!("{t}"); // 100.0°C (Display)
println!("{t:?}"); // Debug format
println!("{t:#?}"); // Pretty-print Debug
}Trait objects with dyn
When you need a collection of different types sharing a trait, use trait objects:
trait Drawable {
fn draw(&self);
}
struct Circle { radius: f64 }
struct Square { side: f64 }
struct Triangle { base: f64, height: f64 }
impl Drawable for Circle {
fn draw(&self) { println!("Drawing circle (r={})", self.radius); }
}
impl Drawable for Square {
fn draw(&self) { println!("Drawing square (s={})", self.side); }
}
impl Drawable for Triangle {
fn draw(&self) { println!("Drawing triangle (b={}, h={})", self.base, self.height); }
}
fn draw_all(shapes: &[Box<dyn Drawable>]) {
for shape in shapes {
shape.draw();
}
}
fn main() {
// Vec of different types that implement Drawable
let canvas: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Square { side: 3.0 }),
Box::new(Triangle { base: 4.0, height: 6.0 }),
Box::new(Circle { radius: 2.5 }),
];
draw_all(&canvas);
}The most important traits from std
| Trait | Purpose |
|---|---|
Clone |
Explicit value copy |
Copy |
Implicit copy (stack only) |
Debug |
Debug formatting {:?} |
Display |
User-facing formatting {} |
PartialEq / Eq |
Equality comparison == |
PartialOrd / Ord |
Ordering comparison <, > |
Hash |
For use as HashMap key |
Iterator |
Iteration protocol |
From / Into |
Conversions between types |
Default |
Default value |
Send / Sync |
Concurrency safety |
Traits are the foundation of all abstractions in Rust. The next lesson explores generics — how to write code that works with multiple types without duplication.
impl Trait vs dyn Trait
impl Trait in parameters or return positions uses static dispatch (monomorphization) — the compiler generates type-specific code for maximum performance. dyn Trait uses dynamic dispatch — a fat pointer including a vtable, useful when you don't know the type at compile time.
Trait objects with Box<dyn Trait>
When you need collections of different types sharing a trait, use Box<dyn Trait>: let items: Vec<Box<dyn Describable>> = vec![Box::new(book), Box::new(movie)]. Each element can be a different type.
use std::fmt;
// Define a trait
trait Describable {
fn description(&self) -> String;
// Method with default implementation
fn short_description(&self) -> String {
let d = self.description();
if d.len() > 50 {
format!("{}...", &d[..47])
} else {
d
}
}
}
struct Book {
title: String,
author: String,
pages: u32,
}
struct Movie {
title: String,
director: String,
duration_min: u32,
}
impl Describable for Book {
fn description(&self) -> String {
format!("'{}' by {} ({} pages)", self.title, self.author, self.pages)
}
}
impl Describable for Movie {
fn description(&self) -> String {
format!("'{}' directed by {} ({} min)", self.title, self.director, self.duration_min)
}
}
fn print_description(item: &impl Describable) {
println!("{}", item.description());
}
fn main() {
let book = Book {
title: String::from("The Lord of the Rings"),
author: String::from("J.R.R. Tolkien"),
pages: 1178,
};
let movie = Movie {
title: String::from("Inception"),
director: String::from("Christopher Nolan"),
duration_min: 148,
};
print_description(&book);
print_description(&movie);
println!("Short: {}", book.short_description());
}
Sign in to track your progress