-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
26: Practice Exercise: Modeling a Shape Hierarchy with Enums
When you're coming from a language like Java or C#, your instinct for handling a "hierarchy" of objects—like different types of shapes—is usually to look for inheritance. Since Rust doesn't have classes or inheritance, I often see developers try to mimic that behavior using a "God Struct." It's a common pattern, but in Rust, it's almost always the wrong way to go.
The "One Size Fits All" Struct
Imagine we're building a simple geometry engine. You might be tempted to create a single Shape struct that contains every possible field any shape could ever need, and then add a tag to tell you which one it actually is. It looks something like this:
enum ShapeType {
Circle,
Rectangle,
}
struct Shape {
shape_type: ShapeType,
radius: Option<f64>,
width: Option<f64>,
height: Option<f64>,
}
At first glance, this feels flexible. You have one type to pass around your application, and you can just check the shape_type to decide which fields to read. I've seen this pattern in legacy C code for decades, and it feels familiar. But here's the problem: you've just created a state space that is far larger than your actual domain.
The Cost of Invalid States
The issue with the struct approach is that it allows for impossible objects. What happens if you create a Shape where the type is Circle, but the radius is None and the width is Some(10.0)? The compiler can't help you here. Your data is lying to you.
Every time you want to calculate an area, you're forced to write defensive code. You can't just use the radius; you have to unwrap() it or handle the None case, even though you know a circle should have a radius. You end up with a codebase littered with .expect("This should never happen") calls. When your types allow for invalid states, you're essentially pushing the burden of correctness from the compiler onto your own memory. That's a losing battle.
Leveraging Algebraic Data Types
This is where Rust's enums—which are actually sum types—really shine. Instead of a struct that might have some data, we use an enum where each variant owns its specific data. This collapses the state space so that only valid shapes can exist.
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Square { side: f64 },
}
Now, it is physically impossible to have a Circle without a radius, or a Circle that accidentally has a width. The data and the label are bound together. When you process these shapes, you use a match statement, and Rust forces you to handle every single variant. If you add a Triangle variant later, the compiler will point to every single match block in your entire project and tell you exactly where you forgot to handle the new shape.
I always tell my juniors: if you find yourself using Option fields in a struct to represent "either-or" data, you don't have a struct; you have an enum that hasn't been written yet.
📋 Practical Task
Implementation: The Geometry Area Calculator
Your task is to build a small area calculation engine using an enum. This will move you away from the "optional field" mindset and into idiomatic Rust sum types.
- Define an enum called
Shapewith the following variants:Circle(containing aradius: f64)Rectangle(containingwidth: f64andheight: f64)Triangle(containingbase: f64andheight: f64)
- Implement a function
calculate_area(shape: &Shape) -> f64that uses amatchstatement to return the correct area for each variant. (Remember: Triangle area is0.5 * base * height). - In your
mainfunction, create aVec<Shape>containing one of each type, iterate through them, and print their areas.
There are no comments for now.