Traits are Rust's superpower for abstraction. This chapter explores advanced trait patterns that enable powerful, flexible, and type-safe designs.
While generics use static dispatch (monomorphization), trait objects enable dynamic dispatch when you need runtime polymorphism:
trait Drawable {
fn draw(&self);
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing circle with radius {}", self.radius);
}
}
impl Drawable for Rectangle {
fn draw(&self) {
println!("Drawing rectangle {}x{}", self.width, self.height);
}
}
// Static dispatch: monomorphized at compile time
fn draw_static<T: Drawable>(item: &T) {
item.draw();
}
// Dynamic dispatch: runtime polymorphism
fn draw_dynamic(item: &dyn Drawable) {
item.draw();
}
fn main() {
let shapes: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 10.0, height: 20.0 }),
];
for shape in shapes {
shape.draw();
}
}
<T: Trait>): Faster (inlined), larger binary, known at compile time&dyn Trait): Smaller binary, slight overhead, heterogeneous collectionsNot all traits can be made into trait objects. A trait is object-safe if:
// Object-safe trait
trait Display {
fn display(&self);
}
// NOT object-safe: has generic method
trait NotObjectSafe {
fn generic<T>(&self, value: T); // โ
}
// NOT object-safe: returns Self
trait AlsoNotObjectSafe {
fn clone_self(&self) -> Self; // โ
}
// Object-safe version
trait ObjectSafeClone {
fn clone_box(&self) -> Box<dyn ObjectSafeClone>;
}
impl<T> ObjectSafeClone for T
where
T: Clone + 'static,
{
fn clone_box(&self) -> Box<dyn ObjectSafeClone> {
Box::new(self.clone())
}
}
use std::fmt::{Debug, Display};
// Multiple bounds inline
fn print_info<T: Debug + Display + Clone>(item: T) {
println!("Debug: {:?}", item);
println!("Display: {}", item);
let copy = item.clone();
}
// Better: use where clause for readability
fn better_print_info<T>(item: T)
where
T: Debug + Display + Clone,
{
println!("Debug: {:?}", item);
println!("Display: {}", item);
}
// Complex bounds with associated types
fn process<T, U>(iter: T)
where
T: Iterator<Item = U>,
U: Display + Clone,
{
for item in iter {
println!("{}", item);
}
}
use std::fmt;
struct Pair<T> {
first: T,
second: T,
}
// Always implement this
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
}
// Only implement for types that support comparison
impl<T: PartialOrd> Pair<T> {
fn largest(&self) -> &T {
if self.first >= self.second {
&self.first
} else {
&self.second
}
}
}
// Blanket implementation: implement for all T that implement Display
impl<T: fmt::Display> fmt::Display for Pair<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.first, self.second)
}
}
Rust allows operator overloading through trait implementation:
use std::ops::{Add, Mul, Neg};
#[derive(Debug, Clone, Copy)]
struct Vector2D {
x: f64,
y: f64,
}
impl Add for Vector2D {
type Output = Self;
fn add(self, other: Self) -> Self {
Vector2D {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
impl Mul<f64> for Vector2D {
type Output = Self;
fn mul(self, scalar: f64) -> Self {
Vector2D {
x: self.x * scalar,
y: self.y * scalar,
}
}
}
impl Neg for Vector2D {
type Output = Self;
fn neg(self) -> Self {
Vector2D {
x: -self.x,
y: -self.y,
}
}
}
fn main() {
let v1 = Vector2D { x: 1.0, y: 2.0 };
let v2 = Vector2D { x: 3.0, y: 4.0 };
let v3 = v1 + v2; // Add
let v4 = v1 * 2.0; // Mul
let v5 = -v1; // Neg
println!("{:?}", v3);
}
use std::ops::{Index, IndexMut};
struct Grid<T> {
data: Vec<T>,
width: usize,
height: usize,
}
impl<T> Index<(usize, usize)> for Grid<T> {
type Output = T;
fn index(&self, (x, y): (usize, usize)) -> &T {
&self.data[y * self.width + x]
}
}
impl<T> IndexMut<(usize, usize)> for Grid<T> {
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut T {
&mut self.data[y * self.width + x]
}
}
fn main() {
let mut grid = Grid {
data: vec![0; 9],
width: 3,
height: 3,
};
grid[(1, 1)] = 5;
println!("{}", grid[(1, 1)]);
}
A trait can require implementors to also implement another trait:
use std::fmt;
// OutlinePrint requires Display
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 2 };
p.outline_print();
}
trait Summary {
fn summarize_author(&self) -> String;
// Default implementation
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
struct Tweet {
username: String,
content: String,
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
// Can override default
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
struct Article {
author: String,
headline: String,
}
impl Summary for Article {
fn summarize_author(&self) -> String {
self.author.clone()
}
// Uses default implementation
}
Add methods to existing types without modifying them:
// Extend Iterator with custom methods
trait IteratorExt: Iterator {
fn my_sum(self) -> Self::Item
where
Self: Sized,
Self::Item: std::ops::Add<Output = Self::Item> + Default,
{
self.fold(Self::Item::default(), |acc, x| acc + x)
}
fn my_collect_vec(self) -> Vec<Self::Item>
where
Self: Sized,
{
let mut vec = Vec::new();
for item in self {
vec.push(item);
}
vec
}
}
// Blanket implementation for all iterators
impl<T: Iterator> IteratorExt for T {}
fn main() {
let sum = (1..=5).my_sum();
println!("Sum: {}", sum);
let vec = (1..=5).my_collect_vec();
println!("{:?}", vec);
}
Traits with no methods that mark types with special properties:
// Define a marker trait
trait Trusted {}
struct SafeData;
struct UntrustedData;
impl Trusted for SafeData {}
// Generic function that requires Trusted
fn process_trusted<T: Trusted>(data: T) {
println!("Processing trusted data");
}
fn main() {
let safe = SafeData;
process_trusted(safe); // OK
let unsafe_data = UntrustedData;
// process_trusted(unsafe_data); // ERROR: not Trusted
}
// Standard library examples:
// - Send: type can be transferred across thread boundaries
// - Sync: type can be referenced from multiple threads
// - Copy: bitwise copy is safe
// - Sized: size known at compile time
While async fn in traits is stabilizing, here's how to use them today:
use async_trait::async_trait;
#[async_trait]
trait AsyncDatabase {
async fn query(&self, sql: &str) -> Result<Vec<String>, String>;
async fn execute(&mut self, sql: &str) -> Result<(), String>;
}
struct PostgresDB {
connection: String,
}
#[async_trait]
impl AsyncDatabase for PostgresDB {
async fn query(&self, sql: &str) -> Result<Vec<String>, String> {
// Simulate async query
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
Ok(vec![format!("Result from: {}", sql)])
}
async fn execute(&mut self, sql: &str) -> Result<(), String> {
println!("Executing: {}", sql);
Ok(())
}
}
#[tokio::main]
async fn main() {
let db = PostgresDB {
connection: "localhost".to_string(),
};
let results = db.query("SELECT * FROM users").await.unwrap();
println!("{:?}", results);
}
Common traits can be automatically derived:
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct User {
id: u64,
name: String,
email: String,
}
// Custom derive with derive_more crate
use derive_more::{Add, Display, From};
#[derive(Debug, Add, Display, From)]
struct Money(u64);
fn main() {
let m1 = Money(100);
let m2 = Money(50);
let total = m1 + m2;
println!("{}", total); // Display
}
dyn Trait