Rust Traits Shared Behavior

A trait defines a set of behaviors that a type can have. Any type that implements a trait promises to provide the functions listed in that trait. Traits are similar to interfaces in other languages — they define what a type can do, not what it contains.

Defining a Trait

trait Greet {
    fn say_hello(&self) -> String;
}

This trait has one requirement: any type that implements Greet must provide a say_hello method that returns a String.

The Job Contract Diagram

trait Driver {
    fn drive(&self);     ← Must be able to drive
}

Any type that signs the Driver contract must provide the drive() function.
Bus, Car, Truck can all implement Driver in their own way.

Implementing a Trait

struct Dog {
    name: String,
}

struct Cat {
    name: String,
}

impl Greet for Dog {
    fn say_hello(&self) -> String {
        format!("Woof! I am {}.", self.name)
    }
}

impl Greet for Cat {
    fn say_hello(&self) -> String {
        format!("Meow! I am {}.", self.name)
    }
}

fn main() {
    let dog = Dog { name: String::from("Rex") };
    let cat = Cat { name: String::from("Luna") };

    println!("{}", dog.say_hello());
    println!("{}", cat.say_hello());
}

Output:

Woof! I am Rex.
Meow! I am Luna.

Default Implementations

A trait can provide a default implementation for a method. Types can use the default or override it:

trait Summary {
    fn title(&self) -> String;

    fn preview(&self) -> String {
        format!("Read more: {}", self.title())   ← Default uses title()
    }
}

struct Article {
    headline: String,
}

impl Summary for Article {
    fn title(&self) -> String {
        self.headline.clone()
    }
    // preview() uses the default — no need to implement it
}

Traits as Function Parameters

Use impl TraitName in a function signature to accept any type that implements the trait:

fn announce(item: &impl Summary) {
    println!("Breaking news: {}", item.title());
}

This function works with any type that implements Summary — you do not need a separate version for every type.

Trait Bounds with Where

For multiple traits or more complex constraints, the where clause keeps the signature readable:

fn compare<T>(a: &T, b: &T) -> bool
where
    T: PartialOrd + std::fmt::Display,
{
    println!("Comparing {} and {}", a, b);
    a < b
}

Common Standard Library Traits

Display — Custom Print Formatting

use std::fmt;

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)
    }
}

fn main() {
    let p = Point { x: 3, y: 7 };
    println!("{}", p);   ← (3, 7)
}

Clone and Copy

Adding #[derive(Clone, Copy)] above a struct automatically implements these traits. Copy types are duplicated instead of moved on assignment.

#[derive(Clone, Copy, Debug)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
}

PartialEq — Equality Comparison

#[derive(PartialEq)]
struct Point { x: i32, y: i32 }

let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
println!("{}", a == b);   ← true

Returning Traits from Functions

Use impl TraitName as a return type when you want to return some type that implements the trait, without specifying the exact type:

fn make_greeting() -> impl Greet {
    Dog { name: String::from("Buddy") }
}

The derive Macro

Many standard traits can be automatically added to your type using the #[derive] attribute. The compiler generates the implementation for you:

#[derive(Debug, Clone, PartialEq)]
struct User {
    name: String,
    age: u32,
}

Quick Reference

trait Name { fn method(&self); }           ← Define a trait
impl Trait for Type { fn method() { } }    ← Implement for a type
fn f(x: &impl Trait) { }                  ← Accept any impl of Trait
#[derive(Debug, Clone)]                    ← Auto-implement common traits

Leave a Comment