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 forCat,Cow, etc.
Rust has two main ways:
| Approach | How it works | Dispatch | When to use |
|---|---|---|---|
Trait objects (dyn Trait) | Runtime decides which method to call | Runtime | Open sets, dynamic types |
| Enums | Pattern match known variants | Compile-time | Closed 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 →
selfpassed 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
| Style | Ownership | Automatic 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());
}
}
