Skip to content

Rust Strings

Text handling is one of the first places Rust feels different from other languages, mostly because it actually has two string types. Once you know why, it stops being confusing.

  • &str (“string slice”) — text that already exists somewhere, usually written directly in your code. Fixed size, can’t grow.
  • String — an owned, growable piece of text. You can add to it, and it lives on the heap.
fn main() {
let literal: &str = "hello"; // a string slice
let owned: String = String::from("hello"); // an owned String
println!("{literal} / {owned}");
}
▶ Try it Yourself

A simple rule of thumb: if you’re just printing fixed text, "like this" (a &str) is fine. If you need to build, grow, or modify text at runtime, reach for String.

fn main() {
let mut greeting = String::from("Hello");
greeting.push_str(", World"); // add a &str
greeting.push('!'); // add a single char
println!("{greeting}");
}

Output:

Hello, World!
▶ Try it Yourself

You can join strings with +, or use format! (which is usually the clearer option):

fn main() {
let first = String::from("Hello, ");
let second = String::from("World!");
let combined = first + &second; // note the & before second
println!("{combined}");
// format! is often easier to read:
let name = "Ferris";
let greeting = format!("Hello, {name}!");
println!("{greeting}");
}
▶ Try it Yourself

A handful you’ll use constantly:

fn main() {
let text = " Hello, Rust! ";
println!("{}", text.trim()); // "Hello, Rust!" (no padding)
println!("{}", text.to_uppercase()); // " HELLO, RUST! "
println!("{}", text.to_lowercase()); // " hello, rust! "
println!("{}", text.contains("Rust")); // true
println!("{}", text.trim().len()); // 12 (length in bytes)
println!("{}", text.trim().replace("Rust", "World")); // "Hello, World!"
}
▶ Try it Yourself

To go character by character, use .chars():

fn main() {
for c in "abc".chars() {
println!("{c}");
}
}
▶ Try it Yourself

Someone typed " rUsT is FUN " into a form. Turn it into rust is great: remove the extra spaces, make it all lowercase, and replace fun with great. Then print the result and its length. Hint: string methods can be chained, one after another: text.trim().to_lowercase()....

Show solution
fn main() {
let input = " rUsT is FUN ";
// trim first, then lowercase, then replace "fun" (now lowercase)
let clean = input.trim().to_lowercase().replace("fun", "great");
println!("{clean}");
println!("{} bytes", clean.len());
}

Output:

rust is great
13 bytes
▶ Try it Yourself

The order matters: if you called .replace("fun", "great") before .to_lowercase(), nothing would be replaced, because the text still says FUN.

Given first = "Ada" and last = "Lovelace", build the String ada.lovelace@example.com and print it. Then print whether it contains an @. Hint: format! can take expressions like first.to_lowercase() as arguments for its {} slots.

Show solution
fn main() {
let first = "Ada";
let last = "Lovelace";
let email = format!("{}.{}@example.com", first.to_lowercase(), last.to_lowercase());
println!("{email}");
println!("looks valid: {}", email.contains('@'));
}

Output:

ada.lovelace@example.com
looks valid: true
▶ Try it Yourself
  • &str is borrowed, fixed text; String is owned and can grow.
  • Grow a String with .push_str() (for text) or .push() (for a single character).
  • Prefer format! over + when combining strings — it’s clearer and doesn’t move anything.
  • Common methods: .trim(), .to_uppercase(), .contains(), .replace(), .chars().

Quick check

1. Which string type is owned and can grow at runtime?

2. Why does this page prefer format! over + when combining more than one or two strings?

3. Why can't you index a Rust string like text[0]?

4. Which method adds a single character to a growing String, like greeting.push('!')?

Score: 0 / 4