Traits and Generics

Traits define shared behavior across types. Generics let you write code that works with multiple types. Together they give Rust powerful abstraction with zero runtime cost.

For Python developers: Traits are similar to Python's 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 defines 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 behavior (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

In the next chapter, we'll explore Rust's module system and how to organize code into 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 { }