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
14: Understanding Ownership
A few years ago, I spent an entire weekend chasing a "use-after-free" bug in a C++ codebase that only triggered under specific load conditions in our staging environment. The code looked perfectly fine. I spent hours staring at pointers, convinced I was losing my mind, only to realize that a temporary object was being deleted by one thread while another thread was still trying to read a property from it. It was a classic memory corruption nightmare—the kind of bug that makes you want to quit the industry and start a farm. When I first encountered Rust, I was skeptical of the "Borrow Checker" because it felt like the compiler was yelling at me for things I thought I had under control. But then it clicked: the compiler wasn't fighting me; it was preventing that weekend of hell from ever happening again.
Moving the Ownership Torch
In most languages you've used, you don't think about who "owns" a piece of memory. You just create an object, pass it around, and the garbage collector cleans it up eventually. Rust doesn't have a garbage collector, but it also doesn't make you manually free memory. Instead, it uses a system of ownership.
The most important rule is this: each value in Rust has a variable that's called its owner, and there can only be one owner at a time. Look at what happens when we work with a String, which is stored on the heap:
let s1 = String::from("Mentorship");
let s2 = s1;
// println!("{}", s1); // This would cause a compile-time error!
In other languages, s2 = s1 would create two references to the same string. In Rust, this is a "move." Because s1 owned the data, and we assigned it to s2, the ownership moved. s1 is now considered uninitialized. If you try to use s1 after that move, the compiler will stop you dead in your tracks. This prevents "double-free" errors where the program tries to clean up the same memory twice when the variables go out of scope.
Borrowing and the Rules of References
Obviously, moving ownership every time you want to pass a string to a function is a pain. You don't want to give away your data just to let a function read it. This is where borrowing comes in. Instead of transferring ownership, you can pass a reference using the & symbol.
fn calculate_length(s: &String) -> usize {
s.len()
}
let s1 = String::from("Hello Rust");
let len = calculate_length(&s1); // We lend the string, we don't give it away
println!("The length of '{}' is {}.", s1, len); // s1 is still valid here!
Now, here is where the Borrow Checker gets strict. You can have as many immutable references (&T) as you want, OR exactly one mutable reference (&mut T) at a time. You cannot have both. I'll be honest: this is the steepest part of the Rust learning curve. But think about why. If you have one part of your code mutating a string while another part is reading it, you've got a data race. By enforcing this at compile time, Rust guarantees that your memory is safe without needing a heavy runtime to manage it.
📋 Practical Task
Exercise: Repairing the Message Processor
You are building a simple message processing system. The following code is failing to compile because the developer tried to print a message after it was already "moved" into the processing function. Your task is to modify the code so that the process_message function borrows the string instead of taking ownership, allowing the main function to print the final confirmation.
fn process_message(msg: String) {
println!("Processing: {}", msg);
}
fn main() {
let my_msg = String::from("System Update Successful");
process_message(my_msg);
// FIX THIS LINE: The compiler says my_msg has been moved
println!("Log completed for: {}", my_msg);
}
Requirements:
- Modify the
process_messagefunction signature to accept a reference. - Update the function call in
mainto pass a reference. - Ensure the program compiles and prints both the "Processing" and "Log completed" messages.
There are no comments for now.