Traits and Generics

Traits define shared behaviour. Generics let you write code that works for many types. Together they give you abstraction with zero runtime cost.

For Python developers: traits are close to Protocols (PEP 544) or abstract base classes. They define an interface that types must implement.

Traits

Defining a Trait

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

Python comparison:

# Using Protocol (Python 3.8+)
from typing import Protocol

class Summary(Protocol):
    def summarize(self) -> str:
        ...

# Or Abstract Base Class
from abc import ABC, abstractmethod

class Summary(ABC):
    @abstractmethod
    def summarize(self) -> str:
        pass

A trait is a set of methods that types can implement. Think of it as an interface.

Implementing a Trait

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

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

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{} by {}", self.title, self.author)
    }
}

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

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

fn main() {
    let article = Article {
        title: "Rust is Great".to_string(),
        author: "Alice".to_string(),
        content: "Long article...".to_string(),
    };

    let tweet = Tweet {
        username: "bob".to_string(),
        content: "Rust rocks!".to_string(),
    };

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

Default Implementations

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

    // Default implementation that calls another trait method
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

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

impl Summary for Article {
    fn summarize_author(&self) -> String {
        self.author.clone()
    }
    // Uses the default summarize()
}

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

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

    // Override the default
    fn summarize(&self) -> String {
        format!("{}: {}", self.summarize_author(), self.content)
    }
}

Multiple Traits

use std::fmt;

trait Printable {
    fn print(&self);
}

trait Saveable {
    fn save(&self) -> Result<(), String>;
}

struct Document {
    content: String,
}

impl Printable for Document {
    fn print(&self) {
        println!("{}", self.content);
    }
}

impl Saveable for Document {
    fn save(&self) -> Result<(), String> {
        println!("Saving: {}", self.content);
        Ok(())
    }
}

impl fmt::Display for Document {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Document: {}", self.content)
    }
}

Common Standard Library Traits

TraitPurposeMethods
DisplayUser-facing string outputfmt()
DebugDeveloper-facing outputfmt() (usually #[derive(Debug)])
CloneExplicit deep copyclone()
CopyImplicit copy (stack-only)(marker trait)
PartialEqEquality comparisoneq()
PartialOrdOrdering comparisonpartial_cmp()
DefaultDefault valuedefault()
IteratorIteration protocolnext()
From / IntoType conversionfrom() / into()
DropCustom cleanupdrop()

Implementing Display

use std::fmt;

struct Point {
    x: f64,
    y: f64,
}

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.0, y: 4.0 };
    println!("{}", p);  // (3.0, 4.0)
}

Implementing From/Into

struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

fn main() {
    let boiling = Celsius(100.0);
    let f: Fahrenheit = boiling.into();  // Uses From automatically
    println!("{}°F", f.0);  // 212°F
}

Generics

Generic Functions

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

    for item in &list[1..] {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    println!("Largest: {}", largest(&numbers));

    let chars = vec!['y', 'm', 'a', 'q'];
    println!("Largest: {}", largest(&chars));
}

Generic Structs

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }
}

// Methods only for specific types
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let int_point = Point::new(5, 10);
    let float_point = Point::new(1.0, 4.0);

    println!("{:?}", int_point);
    println!("Distance: {}", float_point.distance_from_origin());

    // int_point.distance_from_origin();  // ERROR: only for f64
}

Multiple Generic Types

#[derive(Debug)]
struct Pair<T, U> {
    first: T,
    second: U,
}

impl<T, U> Pair<T, U> {
    fn new(first: T, second: U) -> Self {
        Pair { first, second }
    }
}

fn main() {
    let p = Pair::new("hello", 42);
    println!("{:?}", p);

    let p2 = Pair::new(3.14, true);
    println!("{:?}", p2);
}

Generic Enums

You've already been using these:

// From the standard library
enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Trait Bounds

Trait bounds constrain what types a generic can accept.

Basic Trait Bounds

use std::fmt::Display;

// Only types that implement Display
fn print_value<T: Display>(value: T) {
    println!("Value: {}", value);
}

// Multiple bounds with +
fn print_and_compare<T: Display + PartialOrd>(a: T, b: T) {
    if a > b {
        println!("{} is greater", a);
    } else {
        println!("{} is greater", b);
    }
}

fn main() {
    print_value(42);
    print_value("hello");
    print_and_compare(10, 20);
}

Where Clause

When bounds get complex, use where:

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

// This is hard to read
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> String {
    format!("{}", t)
}

// This is cleaner
fn some_function_clean<T, U>(t: &T, u: &U) -> String
where
    T: Display + Clone,
    U: Clone + Debug,
{
    format!("{}", t)
}

Traits as Parameters

Three ways to accept a trait as a parameter:

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

// 1. impl Trait syntax (simple, common)
fn notify(item: &impl Summary) {
    println!("Breaking: {}", item.summarize());
}

// 2. Trait bound syntax (more flexible)
fn notify_bound<T: Summary>(item: &T) {
    println!("Breaking: {}", item.summarize());
}

// 3. Enforcing same type for multiple parameters
fn notify_same<T: Summary>(item1: &T, item2: &T) {
    // item1 and item2 must be the same type
    println!("{} and {}", item1.summarize(), item2.summarize());
}

Returning Traits

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

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

// Return a type that implements Animal
fn create_pet() -> impl Animal {
    Dog
}

fn main() {
    let pet = create_pet();
    println!("{} says {}", pet.name(), pet.sound());
}

Limitation: impl Trait in return position can only return one concrete type.

Trait Objects with dyn

When you need different types at runtime:

trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> &str;
}

struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }

impl Shape for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
    fn name(&self) -> &str { "Circle" }
}

impl Shape for Rectangle {
    fn area(&self) -> f64 { self.width * self.height }
    fn name(&self) -> &str { "Rectangle" }
}

// Trait object with dyn
fn print_area(shape: &dyn Shape) {
    println!("{}: {:.2}", shape.name(), shape.area());
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { radius: 5.0 }),
        Box::new(Rectangle { width: 4.0, height: 6.0 }),
    ];

    for shape in &shapes {
        print_area(shape.as_ref());
    }
}

Static vs Dynamic Dispatch

FeatureStatic (impl Trait / generics)Dynamic (dyn Trait)
DispatchCompile-time (monomorphization)Runtime (vtable)
PerformanceZero-cost, inlinedSmall overhead
Binary sizeLarger (code per type)Smaller
Mixed typesNo (one type per call)Yes (heterogeneous)
Use whenPerformance mattersFlexibility matters

Combining Generics and Traits

Generic Impl with Trait Bounds

#[derive(Debug)]
struct Wrapper<T> {
    value: T,
}

impl<T: std::fmt::Display> Wrapper<T> {
    fn show(&self) {
        println!("Wrapper contains: {}", self.value);
    }
}

impl<T: std::fmt::Display + Clone> Wrapper<T> {
    fn duplicate(&self) -> Self {
        Wrapper {
            value: self.value.clone(),
        }
    }
}

fn main() {
    let w = Wrapper { value: 42 };
    w.show();

    let w2 = w.duplicate();
    w2.show();
}

Blanket Implementations

Implement a trait for all types that satisfy some bounds:

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

// Implement Greet for anything that implements Display
impl<T: std::fmt::Display> Greet for T {
    fn greet(&self) -> String {
        format!("Hello, {}!", self)
    }
}

fn main() {
    println!("{}", 42.greet());           // Hello, 42!
    println!("{}", "world".greet());      // Hello, world!
    println!("{}", 3.14.greet());         // Hello, 3.14!
}

Lifetimes with Generics

When generics involve references, you may need lifetime annotations:

// The returned reference lives as long as both inputs
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

// Struct with lifetime and generic
struct ImportantExcerpt<'a, T> {
    part: &'a str,
    metadata: T,
}

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("xyz");
        result = longest(s1.as_str(), s2.as_str());
        println!("Longest: {}", result);
    }

    let excerpt = ImportantExcerpt {
        part: &s1,
        metadata: 42,
    };
    println!("{}: {}", excerpt.part, excerpt.metadata);
}

Practice Exercises

Exercise 1: Describable Trait

Create a Describable trait with a describe() method. Implement it for at least three different structs.

trait Describable {
    fn describe(&self) -> String;
}
// Implement for Book, Movie, Song

Exercise 2: Generic Stack

Implement a generic stack data structure:

struct Stack<T> {
    elements: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Self { /* ... */ }
    fn push(&mut self, item: T) { /* ... */ }
    fn pop(&mut self) -> Option<T> { /* ... */ }
    fn peek(&self) -> Option<&T> { /* ... */ }
    fn is_empty(&self) -> bool { /* ... */ }
}

Exercise 3: Sortable Collection

Write a generic function that takes a mutable slice, sorts it, and returns the median value:

fn median<T: PartialOrd + Clone>(data: &mut [T]) -> Option<T> {
    // Your code
}

Common Mistakes

1. Missing Trait Bounds

fn print_it<T>(value: T) {
    println!("{}", value);  // ERROR: T doesn't implement Display
}

Fix: Add T: std::fmt::Display.

2. Returning Different Types with impl Trait

fn make_shape(circle: bool) -> impl Shape {
    if circle {
        Circle { radius: 5.0 }
    } else {
        Rectangle { width: 3.0, height: 4.0 }  // ERROR: different type
    }
}

Fix: Use Box<dyn Shape> instead.

3. Orphan Rule

// Can't implement external trait for external type
impl Display for Vec<i32> { }  // ERROR

Fix: Create a wrapper type or your own trait.

Key Takeaways

  • Traits define shared behaviour (like interfaces)
  • Generics let you write type-agnostic code
  • Trait bounds constrain what generics can do
  • Use impl Trait for simple cases, dyn Trait for heterogeneous collections
  • Static dispatch (generics) has zero runtime cost
  • Dynamic dispatch (dyn) adds flexibility at a small performance cost
  • Blanket implementations apply traits broadly
  • Derive macros auto-implement common traits

Next Steps

Continue to 10-modules-crates.md to learn how to organise code into modules, crates, and packages.

Quick Reference

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

// Implement a trait
impl Summary for MyType {
    fn summarize(&self) -> String { ... }
}

// Generic function with trait bound
fn process<T: Display + Clone>(item: T) { }

// Where clause
fn process<T>(item: T) where T: Display + Clone { }

// impl Trait parameter
fn notify(item: &impl Summary) { }

// impl Trait return
fn create() -> impl Summary { }

// Trait object
fn process(item: &dyn Summary) { }
let items: Vec<Box<dyn Summary>> = vec![];

// Lifetime with generic
fn longest<'a, T>(x: &'a str, extra: T) -> &'a str { }