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