Today was all about understanding enums, how they differ from structs, how they model real-world data better in some cases, and why Option is Rust’s safe alternative to null.
📚 What Are Enums?
While structs let you group multiple pieces of data, enums let you express a value that could be one of many types.
Think of enums as a way to say: "This value is either this, that, or something else."
enum IpAddrKind {
V4,
V6,
}
With this, you can create variables like:
let home = IpAddrKind::V4;
let loopback = IpAddrKind::V6;
All values created with the enum are of the same type (IpAddrKind), even though they hold different "meanings".
🧭 Enums vs Structs
You can achieve the same goal using structs and enums. For example:
struct IpAddr {
kind: IpAddrKind,
address: String,
}
But with enums, you can directly associate data with each variant:
enum IpAddr {
V4(String),
V6(String),
}
Or even mix types:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
This keeps your data structure concise and flexible.
🧰 Enums with Data
Rust enums are powerful because variants can:
- Contain different types of data.
- Contain different amounts of data.
Example:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Here:
- Quit is like a unit struct (no data),
- Move acts like a named struct,
- Write is a tuple struct with one value,
- ChangeColor is a tuple struct with three values.
You could also use four separate structs—but then you’d have to write separate logic for each. With enums, you get one unified type.
🧠 Defining Methods on Enums
Just like structs, you can define methods on enums using impl.
impl Message {
fn call(&self) {
println!("Called: {:?}", self);
}
}
And use it like:
let m = Message::Write(String::from("hello"));
m.call();
🔎 The Option Enum
Rust doesn’t have null. Instead, it has the Option enum:
enum Option<T> {
Some(T),
None,
}
This is Rust’s way of saying: "The value might be something or nothing."
Examples:
let some_number = Some(5); // Option<i32>
let some_char = Some('e'); // Option<char>
let absent_number: Option<i32> = None;
This avoids a billion-dollar problem of null values that plague other languages.
🚫 Option Prevents Dangerous Assumptions
Rust won’t let you do this:
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y; // ❌ ERROR
Why? Because Option and i8 are different types. You must explicitly handle the case when a value might be None.
This forces you to safely unwrap or pattern match:
let y: Option<i8> = Some(5);
match y {
Some(n) => println!("Got: {}", n),
None => println!("No value!"),
}
🆚 Option vs Null (Why It’s Better)
- Null is unchecked: you often forget to handle it, leading to runtime crashes.
- Option is checked: the compiler forces you to handle the None case.
- With Option, you’re explicit about the possibility of absence.
- This makes your code more reliable, less error-prone, and easier to reason about.
🧩 Enums in the Standard Library
The Rust standard library contains many useful enums, like:
enum Result<T, E> {
Ok(T),
Err(E),
}
Or the IpAddr type:
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
You can use any data in enums: primitives, structs, or even other enums!
✍️ Summary
Here’s what I covered on Day 7:
- Enums allow expressing one of many possible states or variants.
- You can attach data of varying types to enum variants.
- Enums are more appropriate than structs in many cases (like IpAddr).
- You can define methods on enums using impl, just like structs.
- Rust replaces unsafe null values with the safe and expressive Option.
- Compiler forces you to handle the absence of values properly.
- Option and pattern matching are key to writing robust Rust code.
Rust’s enums feel like algebraic data types from functional programming—powerful, precise, and safe. This was a big shift in thinking from JavaScript-style nullables, and I’m loving it.
Top comments (0)