Strings and Text
Rust's approach to strings surprises many newcomers. Instead of one string type, there are two — and understanding why unlocks a deeper understanding of ownership, memory, and Unicode. Once it clicks, you'll appreciate how much safety Rust gives you for free.
Two String Types
Rust has two primary string types:
String— a heap-allocated, growable, owned string&str— a string slice, a borrowed reference to UTF-8 data
Think of &str like a window into existing string data (which may live in compiled binary or on the heap), while String is a fully owned buffer you can modify.
fn main() {
// &str: a string slice, typically a reference to static data
let greeting: &str = "Hello, world!";
// String: heap-allocated, owned, and growable
let mut name: String = String::from("Alice");
name.push_str(" Smith");
println!("{}", greeting);
println!("{}", name);
// You can get a &str from a String using & or .as_str()
let name_slice: &str = &name;
println!("Slice: {}", name_slice);
}
Creating and Growing Strings
String provides several ways to build and modify text at runtime.
fn main() {
// Different ways to create a String
let a = String::new(); // empty
let b = String::from("hello"); // from literal
let c = "world".to_string(); // via trait method
let d = format!("{} {}", b, c); // formatted
println!("a: {:?}", a);
println!("b: {}", b);
println!("c: {}", c);
println!("d: {}", d);
// Appending to a String
let mut s = String::from("foo");
s.push_str("bar"); // append a &str
s.push('!'); // append a single char
println!("{}", s); // foobar!
// Concatenation with + (moves the left operand)
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 is moved here, s2 is borrowed
println!("{}", s3);
}
String Slices and Indexing
Because Rust strings are UTF-8 encoded, you cannot index them with a plain integer — a single Unicode character can occupy 1 to 4 bytes, so s[0] would be ambiguous.
fn main() {
let s = String::from("hello");
// Use ranges for byte slices (be careful with multi-byte chars!)
let hello = &s[0..3];
println!("{}", hello); // hel
// Iterate over characters safely
let emoji = "Hello 🦀";
for ch in emoji.chars() {
print!("{} ", ch);
}
println!();
// Iterate over raw bytes
for b in "abc".bytes() {
print!("{} ", b);
}
println!();
// Count characters vs bytes
let japanese = "日本語";
println!("chars: {}", japanese.chars().count()); // 3
println!("bytes: {}", japanese.len()); // 9
}
Note: Slicing with byte ranges that split a multi-byte character will panic at runtime. Always prefer
.chars()or.char_indices()when iterating over text with non-ASCII content.
Common String Operations
The String and &str types come with a rich set of methods for searching, splitting, trimming, and transforming text.
fn main() {
let sentence = " Rust makes text handling safe and expressive. ";
// Trim whitespace
let trimmed = sentence.trim();
println!("{:?}", trimmed);
// Check contents
println!("starts with 'Rust': {}", trimmed.starts_with("Rust"));
println!("contains 'safe': {}", trimmed.contains("safe"));
// Split and collect
let words: Vec<&str> = trimmed.split_whitespace().collect();
println!("word count: {}", words.len());
println!("first word: {}", words[0]);
// Replace
let replaced = trimmed.replace("safe", "powerful");
println!("{}", replaced);
// To uppercase / lowercase
println!("{}", "Hello".to_uppercase());
println!("{}", "WORLD".to_lowercase());
// Parse a number from a string
let n: i32 = "42".parse().expect("not a number");
println!("parsed: {}", n);
}
Building Strings Efficiently
When constructing a string from many pieces, avoid repeated + concatenation — each + may allocate. Use format! for small cases, or push into a String buffer for larger ones.
fn main() {
// Inefficient: each + may reallocate
// let result = "a".to_string() + "b" + "c" + "d";
// Better: format! for a few pieces
let (first, last) = ("Jane", "Doe");
let full_name = format!("{} {}", first, last);
println!("{}", full_name);
// Best for many pieces: push into a buffer
let items = vec!["apple", "banana", "cherry"];
let mut result = String::new();
for (i, item) in items.iter().enumerate() {
if i > 0 {
result.push_str(", ");
}
result.push_str(item);
}
println!("{}", result); // apple, banana, cherry
// Or use join() on a slice
let joined = items.join(", ");
println!("{}", joined); // apple, banana, cherry
}
Try It Yourself
Write a function that takes a sentence and returns a new string with every word capitalized (title case). Use .split_whitespace(), .chars(), and string building to construct the result.
fn title_case(s: &str) -> String {
s.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => {
let upper: String = first.to_uppercase().collect();
upper + chars.as_str()
}
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn main() {
let input = "the quick brown fox";
println!("{}", title_case(input)); // The Quick Brown Fox
let input2 = " rust is awesome ";
println!("{}", title_case(input2)); // Rust Is Awesome
}
Try extending this: handle punctuation at the end of words, or make it lowercase all non-first letters before capitalizing.
Key Takeaways
Stringis owned and heap-allocated;&stris a borrowed slice pointing to existing UTF-8 data- Rust strings are always valid UTF-8 — the compiler and runtime enforce this
- You cannot index a string with a plain integer because characters may be multi-byte; use
.chars()instead - Use
format!()or aStringpush buffer to build strings efficiently; avoid chaining+for many pieces - Common methods like
.trim(),.split_whitespace(),.contains(),.replace(), and.parse()cover most real-world needs &Stringcoerces automatically to&str— prefer&strin function parameters for maximum flexibility
Pro Tip: When writing a function that only reads string data, always take
&stras the parameter type rather than&String. A&straccepts both string literals and&Stringreferences (via automatic deref coercion), making your function more reusable without any extra cost.
The lesson covers:
- 5 playground code blocks (two string types, creating/growing, slicing/indexing, common operations, building efficiently)
- Try It Yourself with a realistic
title_casefunction - Key Takeaways with 6 bullets
- Pro Tip blockquote at the end
- All examples are self-contained, idiomatic Rust, and runnable in-browser
Next Steps
With strings mastered, you're ready to learn how to organize your code into modules and use external crates — Rust's package ecosystem.
Next lesson
Modules and Crates
Learn how to organize Rust code with modules and manage dependencies using crates and Cargo
25 min