Lesson 17

String Types

Master Rust's two string types: owned String and borrowed &str, plus UTF-8 handling.

Two Kinds of Strings

Rust has two main string types, and understanding the difference is essential. &str (a "string slice") is a borrowed reference to some UTF-8 text -- string literals like "hello" are &str. String is an owned, heap-allocated, growable string. Think of &str as a read-only view and String as the full, mutable version.

Creating and Building Strings

You can create a String with String::from("text"), "text".to_string(), or String::new() for an empty one. To grow a string, use .push_str() to append a slice, .push() to append a single character, the + operator (which consumes the left operand), or format! for cleaner multi-part concatenation.

No Integer Indexing

You cannot index a Rust string with s[0]. Why? Because Rust strings are UTF-8, and a single character might be more than one byte. Indexing by byte position could land you in the middle of a character. Instead, use .chars() to iterate over Unicode characters, .bytes() for raw bytes, or string slicing like &s[0..4] if you know the byte boundaries.

This might feel annoying at first, but it prevents a whole class of subtle bugs that plague other languages. Rust makes you think about encoding up front so your code handles international text correctly.

Try it

fn main() {
    // --- String vs &str ---
    println!("--- String vs &str ---");
    let literal: &str = "I am a string slice (&str)";
    let owned: String = String::from("I am an owned String");
    println!("  {}", literal);
    println!("  {}", owned);

    // &str -> String
    let promoted: String = literal.to_string();
    println!("  Promoted to String: {}", promoted);

    // String -> &str (automatically via Deref coercion)
    let borrowed: &str = &owned;
    println!("  Borrowed as &str: {}", borrowed);

    // --- Building Strings ---
    println!("\n--- Building Strings ---");
    let mut greeting = String::from("Hello");
    greeting.push(',');
    greeting.push_str(" world");
    greeting.push('!');
    println!("  push/push_str: {}", greeting);

    // + operator (note: consumes the left side)
    let first = String::from("Rust");
    let second = String::from(" rocks");
    let combined = first + &second;
    // first is moved and can no longer be used here
    println!("  + operator: {}", combined);

    // format! is often cleaner for multiple parts
    let name = "Rustacean";
    let version = 2024;
    let formatted = format!("{} v{} -- let's go!", name, version);
    println!("  format!: {}", formatted);

    // --- String Slicing ---
    println!("\n--- String Slicing ---");
    let s = String::from("Hello, Rust!");
    let hello = &s[0..5];
    let rust = &s[7..11];
    println!("  Full: {}", s);
    println!("  Slice [0..5]: {}", hello);
    println!("  Slice [7..11]: {}", rust);

    // --- UTF-8: chars vs bytes ---
    println!("\n--- UTF-8 Handling ---");
    let emoji_text = "Hi 🦀";
    println!("  Text: {}", emoji_text);
    println!("  Byte length: {}", emoji_text.len());
    println!("  Char count: {}", emoji_text.chars().count());

    println!("  Chars:");
    for (i, ch) in emoji_text.chars().enumerate() {
        println!("    [{}] '{}' (Unicode: U+{:04X})", i, ch, ch as u32);
    }

    // --- Useful String Methods ---
    println!("\n--- Useful Methods ---");
    let padded = "  hello, rust!  ";
    println!("  trim: '{}'", padded.trim());
    println!("  contains 'rust': {}", padded.contains("rust"));
    println!("  to_uppercase: {}", padded.trim().to_uppercase());

    let csv = "one,two,three,four";
    let parts: Vec<&str> = csv.split(',').collect();
    println!("  split: {:?}", parts);

    let replaced = csv.replace(",", " | ");
    println!("  replace: {}", replaced);
}

Challenge

Write a function that takes a &str and returns the first word (everything before the first space). Hint: split_whitespace() and next() are your friends.

Previous: Closures & Iterators

Next: Modules & Visibility