Skip to Content
Course content

120: String vs &str In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 String when 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 a Vec<u8> that is guaranteed to be UTF-8.
  • Use &str in 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);
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.