Course contentsShow
← Rust
Lesson 813 of 1,9106. Generics and Static DispatchFree lesson

Modernizing Legacy Code with Generics

Systematically introduce generics to old code to reduce runtime branching and improve performance.

In this lesson, you’ll learn how to transform rigid, runtime-checked legacy code into high-performance, type-safe Rust using generics.

The Legacy Problem: Runtime Branching

Legacy codebases often rely on "enum-based" or "dynamic dispatch" patterns to handle different data types. Imagine a function designed to process different shapes. You might see a pattern like this:

enum Shape {
    Circle(f64),
    Square(f64),
}

fn area(s: Shape) -> f64 {
    match s {
        Shape::Circle(r) => 3.14 * r * r,
        Shape::Square(s) => s * s,
    }
}

This code works, but every time area is called, the CPU must perform a branch check (match). As your system grows, these checks accumulate, leading to "branchy" code that hinders performance and becomes difficult to extend.

Modernizing with Generics

Instead of forcing data into an enum, we define a trait and use generics to resolve the logic at compile time. This is called static dispatch.

First, define the behavior:

trait Area {
    fn calculate(&self) -> f64;
}

Next, implement it for specific types:

struct Circle(f64);
impl Area for Circle { fn calculate(&self) -> f64 { 3.14 * self.0 * self.0 } }

struct Square(f64);
impl Area for Square { fn calculate(&self) -> f64 { self.0 * self.0 } }

Now, replace the legacy function with a generic one:

fn area<T: Area>(shape: T) -> f64 {
    shape.calculate()
}

Why this is Better

When you call area(my_circle), the Rust compiler generates a specialized version of the function specifically for Circle. This process, known as monomorphization, eliminates the match statement entirely. The CPU no longer needs to decide what type it is dealing with at runtime because the logic is baked into the machine code for that specific type.

By moving the decision from runtime to compile time, you achieve two goals: you remove the performance overhead of branching, and you make your system more extensible—users can now implement Area for their own types without ever modifying your original area function.

Key Takeaway: Replace runtime match statements with trait-constrained generics to enable monomorphization, which eliminates branch overhead and creates cleaner, more performant abstractions.