On this page

Traits: abstraction and shared behavior

15 min read TextCh. 4 — Abstractions

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.
rust
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());
}