Rust Strings & Text

Rust has two string flavors: &str — a borrowed, fixed-length slice of text — and String — an owned, growable buffer on the heap. Understanding which one to reach for is the first step toward comfortable text handling.

&str versus String

A string literal like "hello" has type &str: it borrows text that already exists. A String owns its growable heap buffer. The table contrasts the two:

&strString
Borrowed view (a "string slice")Owned, heap-allocated buffer
Fixed length, immutableGrowable, mutable
Cheap to copy (pointer + length)Clone allocates new memory
From literals or slicing a StringFrom String::from, to_string, format!
fn main() {
    let literal: &str = "hello";              // &str: a view over static text
    let owned: String = String::from("hi");   // String: owns a growable buffer
    let from_literal = "hi".to_string();      // same, via the ToString trait

    // a String hands out &str slices cheaply (deref coercion)
    let slice: &str = &owned;

    println!("{} / {} / {} / {}", literal, owned, from_literal, slice);
}

Building Strings

String grows with push (one character) and push_str (a whole slice). For complex formatting, format! builds a new String, and + concatenates from left to right:

fn main() {
    let mut s = String::from("Rust");
    s.push('!');                 // append a single char
    s.push_str(" is fast");      // append a whole &str
    println!("{s}");             // inline-capture format (Rust 1.58+)

    // format! returns an owned String built from a template
    let page = format!("Read {title} by {author}", title = "the Book", author = "Klabnik");
    println!("{page}");

    // '+' takes ownership of the left operand and appends the right
    let greeting = String::from("Hello, ") + "world!";
    println!("{greeting}");
}

Common Methods

Most text work is a chain of small methods. Note that len() counts bytes, not characters — crucial for non-ASCII text:

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

    println!("len (bytes) = {}", hello.len());       // 12
    println!("contains = {}", hello.contains("world"));
    println!("replaced = {}", hello.replace("world", "Rust"));
    println!("uppercase = {}", hello.to_uppercase());

    for word in hello.split(", ") {
        println!("part: {word}");
    }

    let trimmed = "  padded  ".trim();
    println!("trimmed = '{}'", trimmed);
}

UTF-8 and Indexing

Rust strings are valid UTF-8. Because one "character" can span several bytes, indexing by position (s[0]) is a compile error — use chars() for Unicode scalar values or bytes() for raw bytes:

fn main() {
    let s = String::from("Rúst");        // 'ú' is 2 bytes in UTF-8

    println!("bytes = {}", s.len());     // 5, not 4
    for ch in s.chars() {                // iterates 4 chars: R ú s t
        print!("{ch} ");
    }
    println!();
    for b in s.bytes() {                 // iterates 5 raw bytes
        print!("{b} ");
    }
    println!();
}
Practice move: open the lab examples and modify the strings demo — append, format, then iterate chars() vs bytes() on a word like "naïve" and predict each length before running.