Intermediate Topics: Traits and Generics

Intermediate Topics: Traits and Generics

Traits and generics are two of Rust’s most powerful features, enabling you to write flexible, reusable code without sacrificing performance or type safety. They are the foundation of Rust’s unique approach to polymorphism and abstraction.

Generics: Type Parameters for Flexible Code

Generics allow you to write code that works with multiple types while maintaining type safety. Instead of writing separate functions or structs for each type, you can use placeholders for types.

Generic Functions

// Without generics, you might write:
// fn largest_i32(list: &[i32]) -> i32 { /* ... */ }
// fn largest_char(list: &[char]) -> char { /* ... */ }

// With generics:
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0]; // Requires Copy trait

    for &item in list.iter() { // Iterate over references and dereference with &
        if item > largest { // Requires PartialOrd trait
            largest = item;
        }
    }
    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest(&number_list);
    println!("The largest number is {}", result); // Output: 100

    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest(&char_list);
    println!("The largest char is {}", result); // Output: y
}
  • <T>: Declares T as a generic type parameter.
  • T: PartialOrd + Copy: These are trait bounds. They specify that T must implement the PartialOrd trait (for comparison using >) and the Copy trait (because we’re copying elements).

Generic Structs and Enums

Structs and enums can also be generic, allowing them to hold values of different types.

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

// Struct with multiple generic parameters
struct MultiPoint<T, U> {
    x: T,
    y: U,
}

fn main() {
    let integer_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 1.0, y: 4.0 };
    // let mixed_point = Point { x: 5, y: 10.4 }; // ERROR: x and y must be same type for Point<T>

    let mixed_point = MultiPoint { x: 5, y: 10.4 }; // OK for MultiPoint<T, U>
    println!("Integer Point: ({}, {})", integer_point.x, integer_point.y);
    println!("Mixed Point: ({}, {})", mixed_point.x, mixed_point.y);
}

Rust’s standard library makes extensive use of generic enums, such as Option<T> and Result<T, E>.

Performance of Generics (Monomorphization)

One of Rust’s key advantages is that generics have zero runtime cost. The Rust compiler achieves this through a process called monomorphization.

During compilation, for every concrete type that a generic function or struct is used with, the compiler generates a specialized, non-generic version of that code. This means that at runtime, the code behaves exactly as if you had written out duplicate implementations for each type manually, with no overhead from dynamic dispatch. The trade-off can sometimes be a larger binary size due to code duplication.

Traits: Defining Shared Behavior

Traits define shared behavior that types can implement. They are similar to interfaces in other languages but with more powerful features.

Defining a Trait

pub trait Summary {
    fn summarize_author(&self) -> String; // A required method

    // A method with a default implementation
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}
  • pub trait Summary: Declares a public trait named Summary.
  • Methods listed without a body are required to be implemented by any type that implements the trait.
  • Methods with a default implementation can be used as-is or overridden by the implementing type.

Implementing a Trait for a Type

pub struct NewsArticle {
    pub headline: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize_author(&self) -> String {
        format!("{}", self.author)
    }

    // We can also override the default `summarize` method
    fn summarize(&self) -> String {
        format!("{}: {} (by {})", self.headline, self.content, self.author)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

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

    // Use the default summarize implementation for Tweet
}

fn main() {
    let tweet = Tweet {
        username: String::from("rustacean"),
        content: String::from("Of course, as you probably already know, Rust is great!"),
        reply: false,
        retweet: false,
    };
    println!("1 new tweet: {}", tweet.summarize());
    // Output: 1 new tweet: (Read more from @rustacean...)

    let article = NewsArticle {
        headline: String::from("The Sky is Falling!"),
        author: String::from("John Doe"),
        content: String::from("The sky is not actually falling, but it's an exciting headline!"),
    };
    println!("New article: {}", article.summarize());
    // Output: New article: The Sky is Falling!: The sky is not actually falling, but it's an exciting headline! (by John Doe)
}

Traits as Parameters (Trait Bounds)

You can use traits to define trait bounds on generic type parameters, ensuring that the generic type implements certain behaviors.

impl Trait Syntax

This is a concise way to specify trait bounds for function parameters.

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

// This is syntactic sugar for:
// pub fn notify<T: Summary>(item: &T) { ... }

Generic Type Parameters with Trait Bounds

For more complex scenarios, explicitly naming the generic type parameter is clearer.

pub fn notify_verbose<T: Summary>(item: &T) {
    println!("Breaking news (verbose)! {}", item.summarize());
}

Multiple Trait Bounds

You can require a type to implement multiple traits using the + syntax.

use std::fmt::Display;

pub fn notify_multiple_bounds<T: Summary + Display>(item: &T) {
    println!("Breaking news! {}", item.summarize());
    println!("Also displayable: {}", item); // Requires Display trait
}

// `Display` is implicitly implemented for `NewsArticle` through `fmt::Debug` in some cases
// or you can implement it manually
impl Display for NewsArticle {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "Article: {}", self.headline)
    }
}

// You can't use notify_multiple_bounds with Tweet unless Tweet also implements Display
// (which it currently doesn't without explicit impl Display for Tweet)
// `println!` works for `Tweet` because `Debug` is derived, not `Display`.

where Clauses

For many generic parameters or complex trait bounds, a where clause improves readability.

fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + std::fmt::Debug,
{
    // ... function body
    println!("t: {} (cloned: {:?}), u: {:?} (cloned: {:?})", t, t.clone(), u, u.clone());
    42
}

fn main() {
    let num = 10;
    let text = String::from("hello");
    // This will work if NewsArticle (or another type) implements Display + Clone
    // And if `String` implements Clone + Debug (which it does)
    // let _val = some_function(&article, &text);
}

Returning Types with impl Trait

You can also use impl Trait in the return position of a function when you want to return a type that implements a certain trait, but you don’t want to expose the concrete type. This is especially useful for returning iterators or futures.

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from("of course, as you probably already know, people"),
        reply: false,
        retweet: false,
    }
}

// Note: This only works if the function returns *one specific concrete type*
// that implements the trait. You can't return a `Tweet` in one branch and a `NewsArticle` in another.
/*
fn returns_summarizable_error(switch: bool) -> impl Summary {
    if switch {
        Tweet { // ... }
    } else {
        NewsArticle { // ... } // ERROR: `if` and `else` have incompatible types
    }
}
*/

Associated Types in Traits

Associated types allow you to specify a placeholder type within a trait definition. The concrete type for this placeholder is determined by the implementing type. This is often used when a trait logically implies a single, consistent output type.

The most common example is the Iterator trait:

pub trait Iterator {
    type Item; // Associated type
    fn next(&mut self) -> Option<Self::Item>;
}

Here, Item is the type of the elements produced by the iterator. A specific type (e.g., Vec<i32>) will implement Iterator only once, and Item will be fixed (e.g., i32). This removes ambiguity compared to trait Iterator<T>, which would allow multiple implementations for the same type (e.g., Vec<i32> could iterate over i32 or String if allowed, causing confusion).

Trait Objects for Dynamic Dispatch

While generics and trait bounds enable static dispatch (monomorphization at compile time), trait objects (&dyn Trait or Box<dyn Trait>) allow for dynamic dispatch (method resolution at runtime). This provides runtime polymorphism, similar to virtual functions in C++ or interfaces in Java/Go.

Trait objects store:

  1. A pointer to an instance of a type that implements the trait.
  2. A pointer to a vtable (virtual table) that contains pointers to the actual method implementations for that specific type.
// Our Summary trait defined earlier

fn print_summaries(items: &[&dyn Summary]) { // Takes a slice of trait objects
    for item in items {
        println!("- {}", item.summarize());
    }
}

fn main() {
    let tweet = Tweet {
        username: String::from("rustacean"),
        content: String::from("learning Rust is a journey"),
        reply: false,
        retweet: false,
    };

    let article = NewsArticle {
        headline: String::from("New discoveries in systems programming"),
        author: String::from("Dr. Ada Lovelace"),
        content: String::from("Rust provides powerful tools for building safe and fast systems."),
    };

    // Create a vector of trait objects
    let items_to_summarize: Vec<&dyn Summary> = vec![
        &tweet, // &Tweet converts to &dyn Summary
        &article, // &NewsArticle converts to &dyn Summary
    ];

    print_summaries(&items_to_summarize);
}
  • &dyn Summary: This is a trait object. It means “a reference to some type that implements the Summary trait”. The concrete type is determined at runtime.
  • Dynamic dispatch incurs a small runtime overhead due to the vtable lookup, but offers greater flexibility in handling heterogeneous collections.

Object Safety

Not all traits can be turned into trait objects. A trait is object safe if all the methods in the trait have the following properties:

  • The method does not use Self (the concrete type itself) as a parameter.
  • The method does not use Self as a return type (except for Self wrapped in an Option, Result, or Box).

These rules ensure that the size of the implementing type doesn’t need to be known at compile time for the vtable to work correctly.

Blanket Implementations

Rust allows you to implement a trait for any type that satisfies certain trait bounds. These are called blanket implementations.

A common example from the standard library:

// The standard library provides this blanket implementation:
// impl<T: Display> ToString for T {
//     fn to_string(&self) {
//         format!("{}", self)
//     }
// }

This means that any type T that implements the Display trait automatically gets an implementation of the ToString trait. This is why you can call .to_string() on an i32 or a String (which both implement Display).

Exercises / Mini-Challenges

Exercise 8.1: Generic Point Struct with Method

Create a generic Point<T> struct that holds x and y coordinates of type T. Implement a method distance_from_origin(&self) -> f64 for Point<f64> that calculates the Euclidean distance from the origin (0.0, 0.0). This method should only be available for Point<f64>.

Instructions:

  1. Define the Point<T> struct.
  2. Implement impl<T> Point<T> { ... } for a generic constructor new(x: T, y: T) -> Self.
  3. Implement a specific impl Point<f64> { ... } block for the distance_from_origin method.
    • Formula: sqrt(x*x + y*y)
  4. In main, create a Point<f64> and calculate its distance.
  5. Try to call distance_from_origin on a Point<i32> to observe the compiler error.
// Solution Hint:
/*
struct Point<T> {
    x: T,
    y: T,
}

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

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

fn main() {
    let p1 = Point::new(3.0, 4.0);
    println!("Distance from origin: {}", p1.distance_from_origin()); // Output: 5

    let p2 = Point::new(3, 4);
    // p2.distance_from_origin(); // ERROR: Method not found for `Point<i32>`
}
*/

Exercise 8.2: Printable Trait with Default Implementation

Define a trait Printable with a required method get_content(&self) -> String and a default method print_nicely(&self) that uses get_content and println!.

Implement Printable for two different structs: Book (with title: String, author: String) and BlogPost (with headline: String, url: String). For Book, get_content should return “Title by Author”. For BlogPost, get_content should return “Headline (URL)”.

In main, create instances of Book and BlogPost and call print_nicely() on each.

Instructions:

  1. Define the Printable trait.
  2. Define Book and BlogPost structs.
  3. Implement Printable for both structs, providing the get_content method.
  4. In main, instantiate both and call print_nicely().
// Solution Hint:
/*
trait Printable {
    fn get_content(&self) -> String;

    fn print_nicely(&self) {
        println!("--- Content ---");
        println!("{}", self.get_content());
        println!("---------------");
    }
}

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

impl Printable for Book {
    fn get_content(&self) -> String {
        format!("{} by {}", self.title, self.author)
    }
}

struct BlogPost {
    headline: String,
    url: String,
}

impl Printable for BlogPost {
    fn get_content(&self) -> String {
        format!("{} ({})", self.headline, self.url)
    }
    // No need to implement print_nicely as default works
}

fn main() {
    let book = Book {
        title: String::from("The Rust Book"),
        author: String::from("Steve Klabnik & Carol Nichols"),
    };
    book.print_nicely();

    let blog_post = BlogPost {
        headline: String::from("Mastering Rust Traits"),
        url: String::from("https://example.com/rust-traits"),
    };
    blog_post.print_nicely();
}
*/

Exercise 8.3: Dynamic Shape Drawer (Trait Objects)

Define a trait Shape with methods area(&self) -> f64 and draw(&self). Implement Shape for Circle (with radius: f64) and Rectangle (with width: f64, height: f64).

Write a function draw_all_shapes(shapes: Vec<Box<dyn Shape>>) that takes a vector of trait objects and calls both area() and draw() on each shape.

Instructions:

  1. Define the Shape trait.
  2. Define Circle and Rectangle structs.
  3. Implement Shape for both Circle and Rectangle.
    • Circle area: PI * radius * radius
    • Rectangle area: width * height
    • draw should just print a description of the shape and its area.
  4. In main, create a Vec<Box<dyn Shape>>, populate it with instances of Circle and Rectangle (remember Box::new()), and then call draw_all_shapes.
// Solution Hint:
/*
use std::f64::consts::PI;

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

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        PI * self.radius * self.radius
    }

    fn draw(&self) {
        println!("Drawing a Circle with radius {} and area {:.2}", self.radius, self.area());
    }
}

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

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

    fn draw(&self) {
        println!("Drawing a Rectangle with dimensions {}x{} and area {:.2}", self.width, self.height, self.area());
    }
}

fn draw_all_shapes(shapes: Vec<Box<dyn Shape>>) {
    for shape in shapes {
        shape.draw();
        // You could also print area separately if needed: println!("Area: {:.2}", shape.area());
    }
}

fn main() {
    let circle = Circle { radius: 5.0 };
    let rectangle = Rectangle { width: 4.0, height: 6.0 };

    let mut shapes: Vec<Box<dyn Shape>> = Vec::new();
    shapes.push(Box::new(circle));
    shapes.push(Box::new(rectangle));

    draw_all_shapes(shapes);
}
*/