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. 🎯
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 insideOptionorResult.and_then: Chains operations that returnOptionorResult.unwrap_or: Provides a default value ifNoneorErr.
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));
}