🚧 We are still building the lighthouse. Hang tight!

projectlighthouse logoprojectlighthouse

Sign in with your Google or GitHub account to get started

Traits and Generics

Master traits to define shared behavior and generics to write code for any type. Learn trait bounds, derive attributes, and how Rust achieves zero-cost abstractions through static dispatch.

You want to write a function that works with anything that can be printed. Or anything that can be compared for equality. Or anything that can be cloned. Defining separate functions for each concrete type creates duplication. Inheriting from a base class creates fragile hierarchies and couples types to specific inheritance trees.

Traits define shared behavior without inheritance. A trait specifies methods a type must implement. Any type implementing those methods satisfies the trait. Generics let you write functions and structs that work with any type satisfying trait bounds. At compile time, the compiler generates specialized code for each concrete type, achieving abstraction without runtime cost.

Defining Traits

A trait defines a set of methods that types can implement:

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

The trait Summary requires one method: summarize, which takes &self and returns a String. Types implementing Summary must provide this method.

Implementing Traits

Implement a trait for a type using the impl Trait for Type syntax:

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

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

struct Tweet {
    username: String,
    content: String,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.content)
    }
}

Both Article and Tweet implement Summary, each providing their own implementation of summarize. Code can now work with any type implementing Summary without knowing the concrete type.

Using Traits

Call trait methods like regular methods:

let article = Article {
    headline: String::from("Breaking News"),
    content: String::from("Something happened"),
};

println!("{}", article.summarize());

Traits must be in scope to use their methods. Import traits with use:

use crate::Summary;

Default Implementations

Traits can provide default method implementations:

trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

Types can use the default implementation or override it:

impl Summary for Article {
    // Uses default implementation
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.content)
    }
}

Default implementations can call other methods in the trait:

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

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

Types implementing the trait must provide summarize_author. They get summarize automatically, but can override it if needed.

Trait Bounds

Generics let you write functions that work with multiple types. Trait bounds constrain those types to ones implementing specific traits:

fn notify<T: Summary>(item: &T) {
    println!("News: {}", item.summarize());
}

The function notify accepts any type T that implements Summary. The compiler ensures only types with summarize are passed.

Alternative syntax using impl Trait:

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

This is syntactic sugar for the trait bound syntax. Use it for simple cases. For complex bounds or multiple parameters, use the explicit syntax.

Multiple Trait Bounds

Require multiple traits with +:

fn notify<T: Summary + Display>(item: &T) {
    println!("{}", item);
    println!("{}", item.summarize());
}

The type T must implement both Summary and Display.

The where clause makes complex bounds more readable:

fn complex<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    // Function body
}

This is clearer than writing all bounds in the angle brackets.

Returning Types Implementing Traits

Functions can return types that implement a trait without specifying the concrete type:

fn create_summary() -> impl Summary {
    Tweet {
        username: String::from("user"),
        content: String::from("content"),
    }
}

The function signature says it returns something implementing Summary. The implementation returns a Tweet. Callers know only that the return type has a summarize method.

This works only when returning a single concrete type. You can't return different types from different code paths:

fn create_summary(use_tweet: bool) -> impl Summary {
    if use_tweet {
        Tweet { /* ... */ }
    } else {
        Article { /* ... */ } // Error: incompatible types
    }
}

For returning multiple types, use trait objects (covered later).

Common Standard Library Traits

The standard library defines many traits. Understanding common ones helps you write idiomatic Rust.

The Clone trait enables explicit copying:

let s1 = String::from("hello");
let s2 = s1.clone();

The Copy trait enables implicit copying for stack-only data. All primitives implement Copy. Types with heap allocations can't implement Copy.

The Debug trait enables formatting with {:?}:

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

let p = Point { x: 3, y: 5 };
println!("{:?}", p); // Prints: Point { x: 3, y: 5 }

The Display trait enables formatting with {}:

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

println!("{}", p); // Prints: (3, 5)

The PartialEq trait enables equality comparison with == and !=:

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

let p1 = Point { x: 3, y: 5 };
let p2 = Point { x: 3, y: 5 };
assert_eq!(p1, p2);

The PartialOrd trait enables ordering comparisons with <, >, <=, >=.

Deriving Traits

The derive attribute automatically implements certain traits:

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

The compiler generates implementations of Debug, Clone, and PartialEq. This works for traits with standard implementations. Custom behavior requires manual implementation.

Derivable traits include Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash.

Generics

Generic types let you write code that works with multiple types:

struct Point<T> {
    x: T,
    y: T,
}

let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };

The T is a type parameter. Point<i32> and Point<f64> are different types, but both use the same Point definition.

Multiple type parameters:

struct Point<T, U> {
    x: T,
    y: U,
}

let point = Point { x: 5, y: 4.0 };

Generic functions:

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

The function works with any slice of comparable items.

Generic implementations:

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

This implements the x method for Point<T> with any type T.

Implement methods only for specific types:

impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

This implements distance_from_origin only for Point<f64>, not for other types.

Static Dispatch

Rust uses monomorphization for generics. The compiler generates separate code for each concrete type:

let integer = Some(5);
let float = Some(5.0);

The compiler generates two versions of Option: Option<i32> and Option<f64>. Each has specialized code. At runtime, there's no generic Option<T>β€”only concrete types.

This is static dispatch. The compiler determines which code to call at compile time. There's no runtime cost for abstraction. Generic code runs as fast as hand-written specialized code.

Trait Objects and Dynamic Dispatch

When you need to store different types implementing the same trait in a collection, use trait objects:

trait Draw {
    fn draw(&self);
}

struct Circle {}
struct Square {}

impl Draw for Circle {
    fn draw(&self) {
        println!("Drawing circle");
    }
}

impl Draw for Square {
    fn draw(&self) {
        println!("Drawing square");
    }
}

let shapes: Vec<Box<dyn Draw>> = vec![
    Box::new(Circle {}),
    Box::new(Square {}),
];

for shape in shapes {
    shape.draw();
}

The type Box<dyn Draw> is a trait object. It points to any type implementing Draw. The dyn keyword indicates dynamic dispatchβ€”the method to call is determined at runtime.

Trait objects have runtime cost. They use a vtable (virtual table) to look up methods. Each method call through a trait object is an indirect function call. This enables flexibility at the cost of performance.

Use static dispatch (generics with trait bounds) when you know types at compile time. Use dynamic dispatch (trait objects) when you need to store heterogeneous types or the concrete type is unknown until runtime.

Why Traits and Generics Matter

Traits define interfaces without inheritance. Types implement traits independently. A type can implement traits defined in other crates. This avoids the fragility and coupling of inheritance hierarchies.

Generics provide abstraction without overhead. The compiler generates specialized code for each type. You write code once that works with many types, and it compiles to efficient machine code.

Together, traits and generics enable Rust's zero-cost abstractions. Abstract code runs as fast as concrete code. The type system ensures correctness at compile time. The compiler generates optimal code. You achieve both safety and performance.

Notes & Highlights

Β© 2025 projectlighthouse. All rights reserved.