Rust
Completed
-
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
23: The Option and Result Types
Imagine you're using a vending machine. You press the button for a bag of chips. Two things can happen: either the chips fall down, or the slot is empty. In other languages, an empty slot might be represented by a null value, which is basically a landmine waiting to blow up your program the moment you try to "eat" it. In Rust, we don't do that. We use an Option.
Now, imagine a different scenario: you're trying to pay for those chips. You slide a bill into the machine. Either the machine accepts the money (success), or it spits it back out because it's a counterfeit bill or the wrong currency (failure). This isn't just about "nothing" being there; it's about an operation that explicitly failed for a specific reason. That's where Result comes in.
Handling Absence with Option
In Rust, Option<T> is an enum. It tells the compiler (and you) that a value might be missing. It has two variants: Some(T), which holds the data, and None, which represents the absence of data. I've spent years debugging NullPointerExceptions in Java; Rust's approach forces you to deal with the "None" case before the code will even compile.
struct User {
username: String,
email: Option<String>, // Not every user provides an email
}
fn main() {
let user = User {
username: String::from("rustacean_88"),
email: None,
};
match user.email {
Some(mail) => println!("Sending email to {}!", mail),
None => println!("This user has no email on file."),
}
}
Notice how I used a match statement. You can't just call a method on user.email as if it were a string because it isn'tβit's a wrapper. You have to "unwrap" it first.
Handling Failure with Result
While Option is for when something is missing, Result<T, E> is for when something goes wrong. It has two variants: Ok(T) for success and Err(E) for the error. The E part is crucial because it allows you to pass back a specific error message or an error type, rather than just a generic "it didn't work."
I usually use Result whenever I'm doing I/O, like reading a file or hitting an API, because those things fail all the time.
use std::fs::File;
use std::io::{self, Read};
fn read_config_file() -> Result<String, io::Error> {
let mut file = File::open("config.txt")?; // The '?' operator is a lifesaver
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() {
match read_config_file() {
Ok(text) => println!("Config loaded: {}", text),
Err(e) => println!("Failed to load config: {}", e),
}
}
You'll see that ? operator in my code. It's a shorthand that says: "If this result is Ok, give me the value. If it's an Err, return that error from the whole function immediately." It keeps the code from becoming a nested mess of match blocks.
Unwrapping and the Danger of Panic
Sometimes, you're writing a quick prototype or you're 100% certain a value exists. In those cases, you can use .unwrap() or .expect("Message"). This tells Rust: "Just give me the value; if it's None or Err, just crash the program."
I'll be honest: .unwrap() is tempting, but it's a dangerous habit. If your program crashes in production because of a None value you thought was impossible, you're the one getting the 3 AM phone call. Use match, if let, or the ? operator whenever possible.
π Practical Task
Exercise: The Secure Vault Access Checker
You are building a security system for a digital vault. You need to implement a function that checks if a user has access to a specific vault key.
Requirements:
- Create a
Vaultstruct that contains aHashMapwhere the keys are vault IDs (strings) and the values are access codes (integers). - Implement a method called
get_access_codethat takes a vault ID. It should return anOption<i32>(because the vault ID might not exist). - Implement a function called
verify_accessthat takes theOption<i32>returned from the vault and a "provided code" (i32).- If the
OptionisNone, return aResult::Errwith the string "Vault not found". - If the
OptionisSomebut the codes don't match, return aResult::Errwith the string "Invalid access code". - If they match, return a
Result::Okwith the string "Access Granted".
- If the
Your main function should test three cases: a non-existent vault, a vault with the wrong code, and a vault with the correct code, printing the result of each attempt.
There are no comments for now.