Rust Strings

Rust splits text handling between an owned, growable `String` and a borrowed string slice `&str`, and mastering the difference is essential to writing idiomatic code.

Two String Types

Rust has two primary string types that beginners must learn to distinguish. `String` is a heap-allocated, owned, growable, UTF-8 encoded buffer of characters, similar to a resizable text buffer. `&str`, pronounced "string slice", is a borrowed, fixed-size view into UTF-8 text that lives somewhere else — either inside a `String`, or as a `'static` literal embedded directly in the compiled binary.

Creating Strings

String literals written between double quotes, such as `"hello"`, are `&str` slices with a `'static` lifetime. To get an owned, growable `String`, call `.to_string()` or `String::from()` on a literal, or build one up incrementally with `String::new()` followed by `.push_str()` calls.

Creating and Growing a String

fn main() {
    let literal: &str = "hello"; // borrowed slice
    let mut owned: String = String::from(literal); // owned copy

    owned.push_str(", world");
    owned.push('!');

    println!("{}", owned);
    println!("literal is still usable: {}", literal);
}

UTF-8 and Indexing

Rust strings are always valid UTF-8, which means characters can occupy anywhere from one to four bytes. Because of this, Rust deliberately does not allow direct indexing like `my_string[0]` to fetch a character — a byte offset might land in the middle of a multi-byte character and produce garbage. Instead, Rust exposes explicit iterators: `.chars()` for Unicode scalar values and `.bytes()` for raw bytes.

Iterating Over Characters and Bytes

fn main() {
    let word = String::from("caf\u{00e9}"); // "café"

    println!("character count: {}", word.chars().count());
    println!("byte length: {}", word.len());

    for c in word.chars() {
        print!("[{}]", c);
    }
    println!();
}
Note: Slicing a `String` with a byte range, such as `&word[0..3]`, will panic at runtime if the range does not fall on a UTF-8 character boundary. Prefer `.chars()`, `.char_indices()`, or crates built for grapheme handling when working with non-ASCII text.

Common String Methods

  • `.len()` — byte length of the string, not the character count
  • `.push_str(&str)` — appends a string slice to a `String`
  • `.trim()` — returns a slice with leading and trailing whitespace removed
  • `.split(pattern)` — returns an iterator over substrings separated by a pattern
  • `.replace(from, to)` — returns a new `String` with all matches replaced
  • `.to_uppercase()` / `.to_lowercase()` — returns a new, case-converted `String`
  • `.contains(pattern)`, `.starts_with(pattern)`, `.ends_with(pattern)` — boolean pattern checks

Common Method Calls

fn main() {
    let sentence = String::from("  Rust is Fast and Safe  ");

    let trimmed = sentence.trim();
    let upper = trimmed.to_uppercase();
    let words: Vec<&str> = trimmed.split_whitespace().collect();

    println!("trimmed: '{}'", trimmed);
    println!("upper: {}", upper);
    println!("word count: {}", words.len());
    println!("contains 'Fast': {}", trimmed.contains("Fast"));
}
TypeOwnershipTypical use
StringOwned, heap-allocatedBuilding or modifying text at runtime
&strBorrowed, no allocationFunction parameters, literals, read-only views
&StringBorrowed reference to owned dataRarely written directly; auto-derefs to &str
Note: When writing a function that only needs to read a string, accept `&str` rather than `&String` or `String` as the parameter type — it accepts both string slices and references to owned strings, making the function more flexible for callers.

Exercise: Rust Strings

What is the core difference between `String` and `&str`?