Rust Polymorphism – Study Notes

In Rust, there’s no inheritance, no class hierarchies, and no hidden virtual tables — yet you can still write code that acts like classic polymorphism. How? By combining enums, structs, and traits, Rust lets different types share behavior while keeping everything explicit, safe, and lightning-fast.

one interface, many implementations

This approach gives you the flexibility of object-oriented design without the runtime baggage, making Rust’s take on polymorphism both elegant and surprisingly ergonomic.


1. Polymorphism

Definition:

Different types can be treated uniformly through a shared behavior.

  • One interface → many implementations
  • Example: animal.make_sound() works for Cat, Cow, etc.

Rust has two main ways:

ApproachHow it worksDispatchWhen to use
Trait objects (dyn Trait)Runtime decides which method to callRuntimeOpen sets, dynamic types
EnumsPattern match known variantsCompile-timeClosed sets, all variants known upfront

2. Enums with Struct Variants

enum Animal {
    Cat(Cat),
    Cow(Cow),
}
  • Cat(Cat) means the enum variant contains a struct.
  • Allows each struct to have its own fields and trait implementations.
  • Enum wraps multiple types → lets you store different animals together.

Vs bare enum:

enum Animal { Cat, Cow }
  • No struct data.
  • You lose individual trait implementations.

3. Methods vs Free Functions

Method call (animal.make_sound())

  • Owned by the type → self passed automatically
  • Works naturally with traits and trait objects
  • Allows chaining and polymorphism
  • Idiomatic when behavior belongs to a single type

Free function (make_sound(animal))

  • Independent of type → pass object explicitly
  • Useful for:
    • Operations on multiple types
    • Utility / stateless functions
    • Generic functions
  • May need pattern matching for enums
StyleOwnershipAutomatic self?Polymorphism?
animal.make_sound()Method✅ yes✅ via traits
make_sound(animal)Free function❌ no✅ generics, ❌ enum needs match

Rule of thumb (Rust API guidelines):

  • Method → “This object does this”
  • Free function → “This operation involves multiple types or is generic/stateless”

4. Enum + Traits Pattern

enum Animal {
    Cat(Cat),
    Cow(Cow),
}

impl Noise for Cat { fn make_sound(&self) { println!("Miaow"); } }
impl Noise for Cow { fn make_sound(&self) { println!("Moo"); } }

impl Noise for Animal {
    fn make_sound(&self) {
        match self {
            Animal::Cat(c) => c.make_sound(),
            Animal::Cow(c) => c.make_sound(),
        }
    }
}
  • Combines:
    • Concrete structs → encapsulated data & traits
    • Enum → single type to hold multiple animals
    • Static dispatch → fast, safe, no heap needed

5. Key Takeaways

  • Polymorphism: one interface, many implementations
  • Enums: static, closed polymorphism; wrap structs to retain data & trait implementations
  • Methods: ergonomic, owned by type, good for single-type behavior
  • Free functions: flexible, good for multi-type, generic, or stateless operations
  • Rust avoids inheritance; traits + enums = idiomatic alternative

Bonus:

// Polymorphism in Rust: A Complete Guide
// 
// Rust achieves polymorphism through traits and generics,
// rather than traditional class inheritance.

use std::f64::consts::PI;

// Define a trait that acts as an interface
trait Shape {
    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;
    fn describe(&self) -> String;
}

// Struct for Circle
struct Circle {
    radius: f64,
}

// Struct for Rectangle
struct Rectangle {
    width: f64,
    height: f64,
}

// Struct for Triangle
struct Triangle {
    side_a: f64,
    side_b: f64,
    side_c: f64,
}

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

    fn perimeter(&self) -> f64 {
        2.0 * PI * self.radius
    }

    fn describe(&self) -> String {
        format!("Circle with radius {}", self.radius)
    }
}

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

    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }

    fn describe(&self) -> String {
        format!("Rectangle with width {} and height {}", self.width, self.height)
    }
}

// Implement the Shape trait for Triangle
impl Shape for Triangle {
    fn area(&self) -> f64 {
        // Using Heron's formula
        let s = self.perimeter() / 2.0;
        (s * (s - self.side_a) * (s - self.side_b) * (s - self.side_c)).sqrt()
    }

    fn perimeter(&self) -> f64 {
        self.side_a + self.side_b + self.side_c
    }

    fn describe(&self) -> String {
        format!("Triangle with sides {}, {}, {}", self.side_a, self.side_b, self.side_c)
    }
}

// METHOD 1: Static Dispatch with Generics
// Fast, zero-cost abstraction, resolved at compile time
fn print_shape_info<T: Shape>(shape: &T) {
    println!("{}", shape.describe());
    println!("  Area: {:.2}", shape.area());
    println!("  Perimeter: {:.2}", shape.perimeter());
    println!();
}

// METHOD 2: Dynamic Dispatch with Trait Objects
// Flexible, allows heterogeneous collections, resolved at runtime
fn print_shape_info_dynamic(shape: &dyn Shape) {
    println!("{}", shape.describe());
    println!("  Area: {:.2}", shape.area());
    println!("  Perimeter: {:.2}", shape.perimeter());
    println!();
}

fn main() {
    println!("=== Polymorphism in Rust ===\n");

    // Create different shapes
    let circle = Circle { radius: 5.0 };
    let rectangle = Rectangle { width: 4.0, height: 6.0 };
    let triangle = Triangle { side_a: 3.0, side_b: 4.0, side_c: 5.0 };

    // Example 1: Static Dispatch with Generics
    println!("1. Static Dispatch (Generics):");
    print_shape_info(&circle);
    print_shape_info(&rectangle);
    print_shape_info(&triangle);

    // Example 2: Dynamic Dispatch with Trait Objects
    println!("2. Dynamic Dispatch (Trait Objects):");
    print_shape_info_dynamic(&circle);
    print_shape_info_dynamic(&rectangle);
    print_shape_info_dynamic(&triangle);

    // Example 3: Heterogeneous Collection
    // This is only possible with trait objects (dyn)
    println!("3. Heterogeneous Collection:");
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { radius: 3.0 }),
        Box::new(Rectangle { width: 5.0, height: 2.0 }),
        Box::new(Triangle { side_a: 6.0, side_b: 8.0, side_c: 10.0 }),
    ];

    let total_area: f64 = shapes.iter().map(|s| s.area()).sum();
    println!("Total area of all shapes: {:.2}", total_area);
    
    println!("\nAll shapes:");
    for shape in &shapes {
        print_shape_info_dynamic(shape.as_ref());
    }
}
automation

Previous article

AnsibleNew!!