Post

5. Structs and Enums in Rust

🦀 Master Rust structs and enums! Learn custom types, pattern matching, Option/Result, generics, and advanced destructuring for memory-safe systems. 🎯

5. Structs and Enums in Rust

What we will learn in this post?

  • 👉 Defining and Using Structs
  • 👉 Methods and Associated Functions
  • 👉 Enums and Pattern Matching
  • 👉 Option and Result Deep Dive
  • 👉 Struct Update Syntax and Field Init
  • 👉 Generic Structs and Enums
  • 👉 Destructuring and Pattern Matching

Creating Custom Data Types with Structs 🛠️

Structs are the foundation of Rust’s type system, used by companies like Mozilla and AWS to build memory-safe systems where bugs are eliminated at compile time. They let you group related data and behavior together, replacing error-prone manual data handling with type-safe abstractions.

What is a Struct?

A struct is a way to group related data together. Think of it like a user profile or coordinates!

Named Fields

Structs have named fields, making it easy to understand what each piece of data represents.

1
2
3
4
struct UserProfile {
    username: String,
    age: u32,
}

Instantiation

You create a struct by providing values for its fields.

1
2
3
4
let user = UserProfile {
    username: String::from("Alice"),
    age: 30,
};

Accessing Fields

You can access fields using the dot notation.

1
println!("Username: {}", user.username); // Output: Username: Alice

Tuple Struct Variant

You can also create structs without named fields, like this:

1
2
3
4
struct Coordinates(f64, f64);

let point = Coordinates(10.0, 20.0);
println!("X: {}, Y: {}", point.0, point.1); // Output: X: 10, Y: 20

Why Use Structs?

  • Organize Data: Group related information together.
  • Readability: Named fields make your code easier to understand.
graph TD
    A["📦 UserProfile"]:::style1 --> B["👤 username"]:::style2
    A --> C["🎂 age"]:::style3
    D["📋 Coordinates"]:::style4 --> E["➡️ X"]:::style5
    D --> F["⬆️ Y"]:::style2

    classDef style1 fill:#ff6b6b,stroke:#c92a2a,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Happy coding! 😊

Adding Methods to Structs in Rust

In Rust, you can add methods to structs using impl blocks—a pattern used throughout Dropbox’s infrastructure to encapsulate behavior with data, preventing bugs by ensuring methods can only operate on valid state. This is a great way to organize behavior related to your data. Let’s break it down! 😊

Understanding impl Blocks

An impl block allows you to define methods for a struct. Here’s how you can do it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Circle {
    radius: f64,
}

impl Circle {
    // Constructor
    fn new(radius: f64) -> Circle {
        Circle { radius }
    }

    // Method with &self
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }

    // Method with &mut self
    fn set_radius(&mut self, radius: f64) {
        self.radius = radius;
    }
}

Parameter Types Explained

  • &self: This is a reference to the instance of the struct. You can read its data but not change it.
  • &mut self: This allows you to change the struct’s data. Use this when you need to modify the instance.
  • self: This takes ownership of the instance. It’s used when you want to consume the struct.

Organizing Behavior

Using methods helps keep your code clean and organized. For example, you can create a Circle and easily calculate its area or change its radius:

1
2
3
4
5
6
fn main() {
    let mut circle = Circle::new(5.0);
    println!("Area: {}", circle.area());
    circle.set_radius(10.0);
    println!("New Area: {}", circle.area());
}

Understanding Enums in Programming

Enums in Rust are far more powerful than in other languages—Discord uses them extensively for managing game state transitions and message variants at scale, ensuring exhaustive handling prevents runtime crashes. They allow you to define a type that can have several variants with associated data. Let’s explore how to define enums, use variants with data, and perform exhaustive pattern matching.

Defining Enums

In many languages, you can define an enum like this:

1
2
3
4
5
enum TrafficLight {
    Red,
    Yellow,
    Green,
}

Variants with Data

Enums can also hold data. For example, consider a Shape enum:

1
2
3
4
enum Shape {
    Circle(f64), // radius
    Rectangle(f64, f64), // width, height
}

Exhaustive Pattern Matching

You can use match to handle each variant:

1
2
3
4
5
6
fn area(shape: Shape) -> f64 {
    match shape {
        Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
        Shape::Rectangle(width, height) => width * height,
    }
}

This ensures you handle every possible variant, making your code safer and more reliable.

Why Enums Are Powerful

  • Type Safety: Enums prevent invalid values.
  • Clear Intent: They make your code easier to understand.
  • Pattern Matching: You can handle different cases cleanly.

Real-World Example

Think of a payment method:

1
2
3
4
5
enum PaymentMethod {
    CreditCard(String), // card number
    PayPal(String), // email
    Cash,
}

Using enums, you can easily manage different payment types in your application.

graph TD
    A["🏢 Define Enum"]:::style1 --> B["📦 Variants with Data"]:::style2
    B --> C["🔍 Exhaustive Matching"]:::style3
    C --> D["⚡ Powerful Features"]:::style4
    D --> E["🛡️ Type Safety"]:::style5

    classDef style1 fill:#ff6b6b,stroke:#c92a2a,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Enums are a fantastic way to manage complex data types in a clear and safe manner! 😊

Understanding Option and Result<T, E> in Rust</span>

Option and Result are the cornerstones of Rust’s error handling strategy—used by companies like AWS in their SDKs to guarantee no null pointer exceptions or uncaught errors reach production. These enums replace the billion-dollar mistake of null references with explicit, type-safe alternatives.

What are Enums?

Enums in Rust are special types that can hold different values. Two important enums are Option<T> and Result<T, E>.

Option</span>

  • Purpose: Represents an optional value.
  • Variants:
    • Some(value): Contains a value.
    • None: No value.

Example:

1
let maybe_number: Option<i32> = Some(5);

Result<T, E>

  • Purpose: Represents a value that can be successful or an error.
  • Variants:
    • Ok(value): Successful result.
    • Err(error): Error occurred.

Example:

1
let result: Result<i32, &str> = Ok(10);

Combinators and Chaining

Using Combinators

  • map: Transforms the value inside Option or Result.
  • and_then: Chains operations that return Option or Result.
  • unwrap_or: Provides a default value if None or Err.

Example:

1
2
3
let value = Some(3).map(|x| x * 2); // Some(6)
let result = Ok(5).and_then(|x| Ok(x + 5)); // Ok(10)
let default_value = None.unwrap_or(10); // 10

Using the ? Operator

The ? operator simplifies error handling. It returns the value if Ok, or returns the error if Err.

Example:

1
2
3
4
fn get_value() -> Result<i32, &str> {
    let value = Ok(5)?;
    Ok(value + 5)
}

Visual Summary

flowchart TD
    A["🎁 Option<T>"]:::style1 -->|Some| B["✅ Value"]:::style2
    A -->|None| C["❌ No Value"]:::style3
    D["📋 Result<T, E>"]:::style4 -->|Ok| E["✨ Success"]:::style5
    D -->|Err| F["⚠️ Error"]:::style2

    classDef style1 fill:#ff6b6b,stroke:#c92a2a,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Convenient Rust Syntax 🚀

Rust’s expressive syntax for struct updates and field initialization—adopted by companies like Figma for building performant collaborative tools—reduces boilerplate while maintaining memory safety. These patterns are essential for writing idiomatic, maintainable Rust code.

Struct Update Syntax 🛠️

In Rust, you can create a new struct instance based on an existing one using struct update syntax. This is super handy when you want to change just a few fields.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Person {
    name: String,
    age: u32,
}

let alice = Person {
    name: String::from("Alice"),
    age: 30,
};

// Create a new instance based on `alice`
let bob = Person {
    age: 25, // Change only the age
    ..alice  // Use the rest from `alice`
};

Here, bob gets all the fields from alice, except for the age, which we set to 25. This reduces boilerplate code and keeps things clean! ✨

Field Init Shorthand

When the variable names match the field names, you can use field init shorthand. This means you don’t have to repeat yourself!

Example:

1
2
3
4
let name = String::from("Charlie");
let age = 28;

let charlie = Person { name, age }; // No need to write `name: name, age: age`

Benefits:

  • Less code: Reduces repetition.
  • More readable: Makes your code cleaner and easier to understand.

Flowchart of Struct Creation

flowchart TD
    A["🏗️ Create Struct"]:::style1 --> B["🔄 Struct Update"]:::style2
    A --> C["📝 Field Init"]:::style3
    B --> D["✨ New Instance"]:::style4
    C --> D

    classDef style1 fill:#ff6b6b,stroke:#c92a2a,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Using these features makes your Rust code more efficient and enjoyable to write! Happy coding! 🎉

Making Structs and Enums Generic in Rust

Generics in Rust are zero-cost abstractions—companies like Tokio use them extensively to create highly performant async runtimes without runtime overhead. By using type parameters like <T>, you can define structs and enums that work with any data type while maintaining compile-time safety.

What are Type Parameters?

Type parameters are placeholders for types. When you define a struct or enum with a type parameter, you can use it with different types without rewriting the code.

Example of a Generic Struct

1
2
3
4
5
6
7
8
9
// A generic struct that holds a value of any type T
struct Wrapper<T> {
    value: T,
}

fn main() {
    let int_wrapper = Wrapper { value: 42 }; // Holds an integer
    let str_wrapper = Wrapper { value: "Hello" }; // Holds a string
}

Example of a Generic Enum

1
2
3
4
5
6
7
8
9
10
// A generic enum that can hold different types of values
enum Option<T> {
    Some(T),
    None,
}

fn main() {
    let some_number = Option::Some(10); // Holds an integer
    let no_value: Option<i32> = Option::None; // No value
}

When are Generics Useful?

  • Code Reuse: Write once, use with any type.
  • Type Safety: Ensures that the types are correct at compile time.
graph TD
    A["⚙️ Generics"]:::style1 --> B["♻️ Code Reuse"]:::style2
    A --> C["🛡️ Type Safety"]:::style3
    B --> D["🔧 Flexible Code"]:::style4
    C --> E["✅ Compile-Time Checks"]:::style5

    classDef style1 fill:#ff6b6b,stroke:#c92a2a,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

    linkStyle default stroke:#e67e22,stroke-width:3px;

Happy coding! 😊

Advanced Pattern Matching Techniques 🎉

Pattern matching in Rust is exhaustive by design—Mozilla’s Rust compiler literally prevents you from forgetting a case, eliminating entire classes of bugs that plague other systems languages. Let’s explore some advanced techniques that can make your code cleaner and more efficient!

Destructuring Structs and Enums 📦

Destructuring allows you to break down complex data types easily.

1
2
3
struct Point { x: i32, y: i32 }
let point = Point { x: 10, y: 20 };
let Point { x, y } = point; // Now x = 10, y = 20

Use Case 🌟

Use destructuring to extract values from a struct when you need to work with them individually.

@ Bindings 🔗

The @ symbol lets you bind a value while also matching it.

1
2
3
4
5
let value = Some(5);
match value {
    Some(x @ 1..=10) => println!("Value is in range: {}", x),
    _ => println!("Out of range"),
}

Use Case 🚀

This is great for validating ranges while keeping the matched value.

Ignoring Values with _ 🚫

Use _ to ignore values you don’t need.

1
2
3
match (1, 2, 3) {
    (x, _, z) => println!("x: {}, z: {}", x, z),
}

Use Case 🎯

This helps focus on the values you care about without cluttering your code.

Matching Guards 🛡️

Add conditions to your matches for more control.

1
2
3
4
5
let number = 7;
match number {
    n if n % 2 == 0 => println!("Even"),
    _ => println!("Odd"),
}

Use Case 🔍

Use guards to add logic to your matches, making them more dynamic.

Nested Patterns 🏰

You can match patterns within patterns!

1
2
3
4
let tuple = ((1, 2), (3, 4));
match tuple {
    ((x, y), (z, _)) => println!("x: {}, y: {}, z: {}", x, y, z),
}

Use Case 🧩

This is useful for complex data structures, allowing you to extract multiple values at once.


Real-World Production Examples 🏢

1. Discord Message Handler with Enums 💬

Discord’s bot framework uses exhaustive enums to handle different message types safely:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum MessageType {
    Text(String),
    Image(String, u32, u32), // path, width, height
    Embed { title: String, description: String },
    Reaction { emoji: String, count: u32 },
}

fn process_message(msg: MessageType) -> String {
    match msg {
        MessageType::Text(content) => format!("Processing text: {}", content),
        MessageType::Image(path, w, h) => format!("Image {}x{} from {}", w, h, path),
        MessageType::Embed { title, .. } => format!("Embed with title: {}", title),
        MessageType::Reaction { emoji, count } => format!("{} reacted {}", emoji, count),
    }
}

2. AWS SDK Result Type for Error Handling 🔗

AWS SDKs use Result<T, E> extensively to guarantee error safety:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
use std::fmt;

#[derive(Debug)]
struct ApiError {
    code: u32,
    message: String,
}

impl fmt::Display for ApiError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "API Error {}: {}", self.code, self.message)
    }
}

fn fetch_resource(id: u32) -> Result<String, ApiError> {
    if id == 0 {
        return Err(ApiError {
            code: 400,
            message: "Invalid ID".to_string(),
        });
    }
    Ok(format!("Resource {}", id))
}

fn main() {
    match fetch_resource(42) {
        Ok(data) => println!("Success: {}", data),
        Err(e) => eprintln!("Failed: {}", e),
    }
}

3. Tokio Async Pattern with Generic Structs

Tokio uses generic structs for type-safe async operations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct TaskHandler<T> {
    data: T,
    timeout_ms: u64,
}

impl<T: std::fmt::Debug> TaskHandler<T> {
    fn new(data: T, timeout_ms: u64) -> Self {
        TaskHandler { data, timeout_ms }
    }

    fn execute(&self) -> Result<String, &'static str> {
        println!("Processing: {:?}", self.data);
        if self.timeout_ms > 1000 {
            Ok("Completed".to_string())
        } else {
            Err("Timeout exceeded")
        }
    }
}

fn main() {
    let handler = TaskHandler::new(vec![1, 2, 3], 2000);
    match handler.execute() {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}

4. Figma Collaborative State with Struct Updates 🎨

Figma’s real-time collaboration uses struct update syntax for efficient state management:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#[derive(Clone)]
struct Shape {
    id: u32,
    x: f64,
    y: f64,
    width: f64,
    height: f64,
    fill_color: String,
}

fn update_shape_position(shape: Shape, dx: f64, dy: f64) -> Shape {
    Shape {
        x: shape.x + dx,
        y: shape.y + dy,
        ..shape // Efficiently copy remaining fields
    }
}

fn main() {
    let rect = Shape {
        id: 1,
        x: 10.0,
        y: 20.0,
        width: 100.0,
        height: 50.0,
        fill_color: "#ff6b6b".to_string(),
    };

    let moved_rect = update_shape_position(rect, 5.0, 10.0);
    println!("New position: ({}, {})", moved_rect.x, moved_rect.y);
}

5. Mozilla WebAssembly with Option</span> 🦀

Mozilla’s WASM projects use Option extensively for null-safe code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct User {
    id: u32,
    name: String,
    email: Option<String>,
    phone: Option<String>,
}

impl User {
    fn contact_info(&self) -> String {
        match (&self.email, &self.phone) {
            (Some(email), Some(phone)) => format!("Email: {}, Phone: {}", email, phone),
            (Some(email), None) => format!("Email: {}", email),
            (None, Some(phone)) => format!("Phone: {}", phone),
            (None, None) => "No contact info available".to_string(),
        }
    }
}

fn main() {
    let user = User {
        id: 1,
        name: "Alice".to_string(),
        email: Some("alice@example.com".to_string()),
        phone: None,
    };
    
    println!("{}", user.contact_info());
}

6. Dropbox File System Enum Pattern 📁

Dropbox’s file system uses enums for type-safe file operations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum FileEntry {
    File { name: String, size: u64, modified: String },
    Directory { name: String, item_count: u32 },
    Symlink { target: String },
}

fn get_info(entry: &FileEntry) -> String {
    match entry {
        FileEntry::File { name, size, .. } => 
            format!("File: {} ({} bytes)", name, size),
        FileEntry::Directory { name, item_count } => 
            format!("Directory: {} ({} items)", name, item_count),
        FileEntry::Symlink { target } => 
            format!("Link → {}", target),
    }
}

fn main() {
    let file = FileEntry::File {
        name: "document.pdf".to_string(),
        size: 2048000,
        modified: "2026-01-15".to_string(),
    };
    
    println!("{}", get_info(&file));
}

Hands-On Assignment: Build a Type-Safe Data Processing Pipeline 🚀

📋 Your Challenge: Create a Rust Task Management System
## 🎯 Mission Build a production-ready task management system in Rust that demonstrates mastery of structs, enums, pattern matching, and error handling. Your system must manage different task types, handle errors exhaustively, and use generics for reusable components. ## 📋 Requirements **Core Features** (All Required): 1. **Task Struct** - Define a Task with id, title, description, priority (enum), status (enum) 2. **Status Enum** - Pending, InProgress, Completed, Cancelled with associated data 3. **Priority Enum** - High, Medium, Low with different behaviors 4. **Result Type Error Handling** - Create custom TaskError enum for all operations 5. **Generic Task Container** - Container struct that works with any serializable type 6. **Pattern Matching** - Use exhaustive matching for all enum operations 7. **Option Handling** - Manage optional due dates and assignees 8. **Struct Updates** - Update tasks efficiently using field update syntax ## 💡 Hints - Start with enums for Status and Priority - Create a TaskError enum with variants for each error type - Use impl blocks to add methods to Task - Leverage generics for flexible storage - Use match for exhaustive enum handling - Apply the ? operator for error propagation - Test with realistic data (5+ tasks, multiple status changes) ## 📐 Example Project Structure ``` src/ main.rs task.rs (Task struct and impl) status.rs (Status enum) priority.rs (Priority enum) error.rs (TaskError enum) container.rs (Generic Container) ``` ## 🎯 Bonus Challenges **Level 1** 🟢 Add task filtering by priority **Level 2** 🟢 Implement task search by title (Option handling) **Level 3** 🟠 Create task workflow transitions with validation **Level 4** 🟠 Add recurring tasks with enum variants **Level 5** 🔴 Implement dependency tracking between tasks **Level 6** 🔴 Add serialization to JSON with serde (requires generics) ## 📚 Learning Goals After completing this assignment, you will: - ✓ Understand struct composition and method organization - ✓ Master exhaustive enum pattern matching - ✓ Implement proper error handling with Result and custom errors - ✓ Create reusable code with generics - ✓ Write production-quality Rust code ## ⚡ Pro Tip Start with just the basic Task struct and Status enum. Get the core match logic working, then add Priority. Once that's solid, introduce generic Container. Finally, refine error handling—that's when your code becomes bulletproof! ## 🎓 Call-to-Action Build this project and commit to GitHub! This assignment teaches you the patterns used in production systems at Mozilla, AWS, Discord, and beyond. Complete it, then extend it with your own ideas. The mastery of structs and enums is what makes Rust developers highly paid and highly valued. **Get coding!** 💪 </div> </details> --- # Conclusion: Master Rust Structs and Enums 🎓 Structs and enums are the foundation of Rust's type system—they eliminate entire classes of bugs at compile time that plague other languages, making your systems code safer and more reliable than anything written in C or C++. By mastering these concepts alongside exhaustive pattern matching and error handling with Result and Option, you'll write production-grade systems that power companies from Mozilla to AWS to Discord, scaling from prototypes to millions of users without runtime crashes.
This post is licensed under CC BY 4.0 by the author.