Labs ICT
โญ Pro Login

Traits

Shared behavior across different types.

Traits in Rust

Traits define shared behavior that types can implement. They're like interfaces in other languages, but more powerful because they can provide default implementations.

You define a trait with the trait keyword, then implement it for your types with impl Trait for Type.

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

struct Person {
    name: String,
}

impl Greet for Person {
    fn greet(&self) -> String {
        format!("Hello, my name is {}", self.name)
    }
}

fn main() {
    let alice = Person { name: "Alice".to_string() };
    println!("{}", alice.greet());
}
Try it Yourself ->

Trait Bounds on Functions

Trait bounds let you specify what behavior a type must have. This makes your functions more flexible while maintaining type safety.

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

struct Article {
    title: String,
    content: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}: {}", self.title, &self.content[..50])
    }
}

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

fn main() {
    let article = Article {
        title: "Rust is Amazing".to_string(),
        content: "Today we learned about traits and they are incredibly powerful...".to_string(),
    };
    notify(&article);
}
Try it Yourself ->

Multiple Trait Bounds

You can require multiple traits with + syntax or where clauses. This lets you be specific about what your function needs.

use std::fmt::{Display, Debug};

fn compare<T: Display + Debug>(a: &T, b: &T) {
    println!("a: {}, b: {}", a, a);
    println!("a debug: {:?}, b debug: {:?}", a, b);
}

fn main() {
    let x = 42;
    let y = 43;
    compare(&x, &y);
}
Try it Yourself ->

Default Implementations

Traits can provide default method implementations. Types can use these defaults or override them with their own version.

trait Animal {
    fn name(&self) -> &str;
    fn sound(&self) -> &str;

    fn describe(&self) -> String {
        format!("{} says {}", self.name(), self.sound())
    }
}

struct Dog { name: String }
struct Cat { name: String }

impl Animal for Dog {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> &str { "Woof" }
}

impl Animal for Cat {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> &str { "Meow" }

    fn describe(&self) -> String {
        format!("The cat {} is purring", self.name())
    }
}

fn main() {
    let dog = Dog { name: "Rex".to_string() };
    let cat = Cat { name: "Whiskers".to_string() };

    println!("{}", dog.describe());
    println!("{}", cat.describe());
}
Try it Yourself ->

Trait Objects for Dynamic Dispatch

Trait objects (dyn Trait) allow you to use different types through a single interface. This enables polymorphism at runtime.

trait Drawable {
    fn draw(&self);
}

struct Circle { radius: f64 }
struct Square { side: f64 }

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}", self.radius);
    }
}

impl Drawable for Square {
    fn draw(&self) {
        println!("Drawing a square with side {}", self.side);
    }
}

fn main() {
    let shapes: Vec<Box<dyn Drawable>> = vec![
        Box::new(Circle { radius: 5.0 }),
        Box::new(Square { side: 4.0 }),
    ];

    for shape in &shapes {
        shape.draw();
    }
}
Try it Yourself ->

Common Traits

Rust has many built-in traits that you'll use frequently. Display for user-facing output, Debug for debugging, Clone for copying, and PartialEq for comparison.

Deriving these traits is often as simple as adding #[derive(TraitName)] above your struct. This saves you from writing boilerplate code.

๐Ÿงช Quick Quiz

What trait defines shared behavior?