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
120: String vs &str In Depth
I've seen a lot of developers hit a wall when they first start treating strings in Rust as "just strings." Coming from Python or JavaScript, you expect a string to just be a string. But in Rust, we have String and &str, and if you use the wrong one at the wrong time, the compiler will start shouting at you about lifetimes and ownership.
Let's just dive into some code and see where it breaks. I want to write a small utility that takes a raw username from a config file and "sanitizes" it by trimming whitespace and converting it to lowercase.
The "Why won't this just work?" Moment
fn sanitize_username(name: String) -> &str {
let cleaned = name.trim().to_lowercase();
&cleaned
}
fn main() {
let user = String::from(" Alice_Dev ");
let result = sanitize_username(user);
println!("Cleaned name: {}", result);
}
If you try to compile this, Rust is going to throw a fit. Specifically, it'll tell you that &cleaned doesn't live long enough. Let's look at why. name.trim().to_lowercase() creates a brand new String because lowercase conversion often changes the byte length of the text. That new String is stored in the variable cleaned, which is local to the function.
I'm trying to return a &str (a slice/reference) to that local variable. But as soon as the function hits that closing curly brace, cleaned is dropped. If Rust let me return that reference, I'd be pointing to a piece of memory that's already been freed. That's a classic use-after-free bug, and Rust's borrow checker is just doing its job by stopping me.
Owning the Result
To fix this, I can't return a reference to something I just created inside the function. I need to return the actual ownership of the data. I'll change the return type to String.
fn sanitize_username(name: String) -> String {
let cleaned = name.trim().to_lowercase();
cleaned // No ampersand here; we are moving the value out
}
Now the code compiles. But wait—I've noticed something annoying. Because sanitize_username takes a String, it takes ownership of the input. If I wanted to use the original user variable later in main, I couldn't. I'd have to .clone() it, which is a waste of memory.
Making the Input Flexible
Do I actually need to own the input string just to trim it? No. I just need to look at it. This is where &str shines as an input type. Let's tweak the signature:
fn sanitize_username(name: &str) -> String {
name.trim().to_lowercase()
}
fn main() {
let user = String::from(" Alice_Dev ");
// Now I pass a reference using &
let result = sanitize_username(&user);
println!("Original: {}", user); // This still works!
println!("Cleaned: {}", result);
}
By changing the input to &str, the function is now way more flexible. It doesn't care if you pass it a String (via &user) or a hard-coded string literal like " Bob_Dev ". Both of those can be viewed as a &str.
When to use which?
After messing with this for a while, here is the rule of thumb I use in my own projects:
- Use
Stringwhen you need to own the data, mutate it (like adding characters), or return a value that was created inside a function. Think of it as aVec<u8>that is guaranteed to be UTF-8. - Use
&strin function arguments whenever possible. It's basically a "view" into a string. It's cheaper because it doesn't copy the data; it just holds a pointer and a length.
If you're ever stuck, just ask yourself: "Do I need to own this memory, or am I just visiting it?" If you're just visiting, use the ampersand.
📋 Practical Task
The User Profile Validator Refactor
You are reviewing a teammate's code for a user profile system. The current implementation is inefficient because it forces the caller to clone strings unnecessarily and has a few ownership bugs.
Your Task: Refactor the following code.
1. Change the validate_username function to accept a &str instead of a String to avoid taking ownership.
2. Fix the format_display_name function so it returns a String instead of a &str (since it creates a new string by combining a greeting and the name).
3. Ensure the main function can still print the original username after the functions have been called.
fn validate_username(name: String) -> bool {
// Check if name is at least 3 chars long
name.len() >= 3
}
fn format_display_name(name: &str) -> &str {
let greeting = String::from("User: ");
let combined = greeting + name;
&combined // This is currently causing a compiler error!
}
fn main() {
let username = String::from("Rustacean");
if validate_username(username) {
let display = format_display_name(&username);
println!("{}", display);
}
println!("The original username was: {}", username);
}There are no comments for now.