DaleSchool

Understanding Strings: String and &str

Intermediate15min

Learning Objectives

  • Explain the difference between String and &str
  • Create and manipulate String values (String::from(), push_str(), format!)
  • Understand what a string slice (&str) is
  • Choose the right way to pass strings to functions

Working Code

You've used strings in two different ways so far. Let's see the difference:

fn main() {
    // Approach 1: just double quotes
    let greeting = "hello";

    // Approach 2: String::from()
    let mut name = String::from("Alice");
    name.push_str("!");

    println!("{greeting}, {name}");
}

"hello" works on its own — so why do you sometimes need String::from()?

Try It Yourself

  1. Try calling push_str("!") on greeting. What error do you get?
  2. Change name to let name = "Alice"; and try push_str. What happens?

"Why?" — Two String Types Exist for a Reason

From an ownership perspective, it's clear:

| | String | &str | | --------- | ---------------- | --------------------------------------------- | | Analogy | A string you own | A borrowed string | | Storage | Heap (resizable) | Read-only memory or a slice of another String | | Mutable | Yes, if mut | No | | Ownership | Owned | None (just a reference) |

A string literal like "hello" is type &str. It's baked into the program binary and can't be modified. You're borrowing it.

String::from("hello") copies that content to the heap and makes it yours. Since you own it, you can modify it.

Ways to Create a String

fn main() {
    // Method 1: String::from()
    let s1 = String::from("hello");

    // Method 2: .to_string()
    let s2 = "hello".to_string();

    // Method 3: format! macro
    let name = "Alice";
    let age = 25;
    let s3 = format!("{name} is {age} years old");

    println!("{s1}, {s2}, {s3}");
}

All three produce a String. format! is handy for combining multiple values.

Modifying a String

fn main() {
    let mut s = String::from("hel");

    s.push_str("lo");         // append a string slice
    s.push('!');              // append a single character

    println!("{s}");           // "hello!"
    println!("Length: {}", s.len()); // byte count
}

push_str() takes a &str and appends it. push() adds a single char.

String Slices — Borrowing a Portion

&str can also point to part of a string:

fn main() {
    let sentence = String::from("hello world");

    let hello = &sentence[0..5];   // "hello"
    let world = &sentence[6..11];  // "world"

    println!("{hello}, {world}");
}

&sentence[0..5] borrows bytes 0 through 4 from sentence. It doesn't create a new String — it references a portion of the original.

Function Parameters — &str Is More Flexible

When accepting strings in a function, prefer &str:

fn greet(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let owned = String::from("Alice");
    let literal = "Bob";

    greet(&owned);    // String passed as &str — works!
    greet(literal);   // &str works too, of course!
}

Using &str as a parameter accepts both String and string literals. If you used &String, you could only accept String. That's why &str is the more flexible choice.

Learn More: Converting Between String and &str

Here's how to go back and forth between String and &str:

fn main() {
    // &str -> String
    let s: String = "hello".to_string();
    let s2: String = String::from("hello");

    // String -> &str
    let borrowed: &str = &s;      // automatic coercion
    let slice: &str = s.as_str(); // explicit conversion

    println!("{borrowed}, {slice}");
}

Converting String to &str is just borrowing — zero cost. Converting &str to String copies data to the heap — there is a cost.

Exercise 1. Complete the function below. It takes a name and age, then returns a greeting string.

fn make_greeting(name: &str, age: i32) -> String {
    // Return "Hello, {name}! You're {age} years old!"
    // Hint: use the format! macro
}

fn main() {
    let msg = make_greeting("Alice", 25);
    println!("{msg}");
}

Exercise 2. Fix the error below. Try to solve it without using clone().

fn print_length(s: String) {
    println!("Length: {}", s.len());
}

fn main() {
    let word = String::from("Rust");
    print_length(word);
    println!("Word: {word}"); // error!
}

Hint: How can you stop the function from taking ownership? Change the parameter type.

Q1. What is the biggest difference between String and &str?

  • A) String stores numbers; &str stores characters
  • B) String is owned and mutable; &str is a borrowed reference
  • C) String is slow; &str is fast
  • D) No difference — they are the same type

Q2. Why is &str a good choice for function parameters?

  • A) It's faster
  • B) It can accept both String and &str, making it flexible
  • C) It never produces errors
  • D) It lets you modify the string

Q3. What does this code print?

fn main() {
    let s = String::from("hello world");
    let word = &s[0..5];
    println!("{word}");
}
  • A) "hello world"
  • B) "hello"
  • C) "h"
  • D) Compile error