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
17: Lifetimes Explained
Look, I'll be honest with you: lifetimes are usually the point where people start to feel like Rust is fighting them. It's the steepest part of the learning curve. But once you stop thinking of them as "magic symbols" and start seeing them as a way of describing a relationship, it all clicks.
Think of a lifetime like a library book loan. When you check out a book, the library gives you a slip of paper (a reference) that says you have access to that specific book. The rule is simple: the slip of paper is only valid as long as the book actually exists in the library's collection. If the library burns down or the book is permanently discarded, that slip of paper becomes a useless piece of trash. If you tried to use it to go get the book, you'd be pointing at a void. In C, you'd just crash or read garbage memory. In Rust, the "Librarian" (the borrow checker) refuses to let you even leave the building if there's any chance the book might be gone before your slip expires.
Mapping the Library to the Code
In Rust, a lifetime is just a name for the scope in which a piece of data is valid. Most of the time, Rust handles this for you automatically through "elision," but when you start passing references into functions and returning references back out, the compiler needs you to be explicit about the relationship.
- The Book is the actual data sitting on the heap or stack.
- The Loan Slip is the reference
&T. - The Loan Period is the lifetime
'a.
When you see 'a, don't think of it as a variable that changes. Think of it as a label. You're telling Rust: "Hey, this returned reference will live at least as long as the shortest-lived input reference."
The "Longest String" Dilemma
Let's look at a specific case where the compiler will yell at you. Imagine you're writing a function that takes two string slices and returns the longer one. You might try this:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
This won't compile. Why? Because the compiler doesn't know if the returned reference points to x or y. If x lives for 10 minutes but y only lives for 5 seconds, and the function returns y, the caller might try to use that reference after 6 seconds—at which point y is gone. The Librarian stops you right there.
To fix it, we use generic lifetime annotations. Notice we aren't changing how long the variables live; we're just describing the relationship:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
By using 'a on all three, you're promising: "The return value will be valid for as long as both x and y are valid." The compiler now knows to truncate the lifetime of the result to the shortest of the two inputs.
Tying Lifetimes to Structs
Functions are one thing, but when you put a reference inside a struct, you're essentially saying this struct is a "loan slip" that depends on some external data. You have to name that dependency.
struct BookReview<'a> {
book_title: &'a str,
review_text: &'a str,
}
If you didn't have 'a here, Rust would be terrified. It wouldn't know if BookReview was outliving the strings it points to. By adding the annotation, you're telling Rust: "This BookReview instance cannot possibly outlive the strings it references." If you try to drop the string but keep the BookReview, the compiler will catch it.
The 'static Escape Hatch
You'll often see 'static. This is a special lifetime that lasts for the entire duration of the program. String literals are the most common example. When you write let s = "Hello";, that string is baked directly into the binary. It never gets dropped, so it's &'static str. It's the only loan that never expires.
📋 Practical Task
Build a Log-Message Parser
Your goal is to create a LogEntry struct that doesn't copy data, but instead holds references to a raw log string. This is a common performance optimization in high-throughput systems to avoid unnecessary allocations.
Requirements:
- Create a struct named
LogEntrythat contains two fields:level(a reference to a string) andmessage(a reference to a string). - Implement a function
parse_log(log: &str) -> LogEntry. - The function should split the input string at the first colon (
:). The part before the colon is thelevel, and the part after is themessage. - Ensure you use the correct lifetime annotations so that the
LogEntrycannot outlive the originallogstring.
Test Case:
fn main() {
let raw_log = String::from("ERROR: Disk full on /dev/sda1");
let entry = parse_log(&raw_log);
println!("Level: {}, Message: {}", entry.level, entry.message);
}There are no comments for now.