Chapter 19: Rust Strings
Rust Strings.
This is one of the topics where almost every new Rust learner says: “Wait… why two types? Why not just one like in Python or Java?”
Rust has two main string types:
- String (owned, growable, heap-allocated)
- &str (borrowed string slice, usually immutable view)
(and rarely str alone, &mut str, etc.)
The reason for two is Rust’s ownership system — it gives you safety + performance + flexibility without garbage collector or manual memory management. Once it clicks, you’ll love it.
Let me explain like your teacher sitting next to you with code open: slowly, with analogies (biryani plate vs photo of biryani), many runnable examples, tables, and real patterns from 2026 Rust code.
1. The Two Main Types — Quick Overview
| Feature | String | &str (most common) |
|---|---|---|
| Ownership | Owns the data (heap allocated) | Borrows (references) existing data |
| Mutability | Can be changed (grow, shrink, mutate) | Immutable view (can’t change the text) |
| Memory location | Heap (dynamic size) | Anywhere (static, heap, stack) |
| When created | At runtime (String::from, .to_string()) | Compile-time literals or slices |
| Typical use | Build/modify strings, store in structs | Function params, read-only views |
| Example literal | String::from(“hello”) | “hello” (type: &’static str) |
| Coercion | Can coerce to &str (.as_str()) | Cannot become String without allocation |
Think of it like this:
- String = you own a full plate of Hyderabadi biryani — you can add more rice, eat some, give it away (move ownership).
- &str = a photo of that biryani plate — you can look at it, show to friends, but you can’t change the food in the photo.
2. String Literals — They Are &str (Not String!)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
fn main() { let literal = "Namaste Hyderabad!"; // type: &'static str println!("Type of literal: {}", std::any::type_name::<&str>()); // prints: &str // You can borrow it many times println!("Length: {}", literal.len()); // 19 } |
- “…” → creates a string literal → stored in binary (static lifetime ‘static)
- Always &’static str — never String
- Zero-cost, super efficient
3. Creating String (Owned)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
fn main() { // Way 1: from literal let owned: String = String::from("Hello"); // Way 2: .to_string() on &str let also_owned = "World".to_string(); // Way 3: empty + push let mut greeting = String::new(); greeting.push_str("Namaste "); greeting.push('🦀'); // single char greeting.push_str(" from Rust!"); println!("Greeting: {}", greeting); // Namaste 🦀 from Rust! } |
- String is growable — has capacity (pre-allocated space) and length
- Methods: .push(char), .push_str(&str), +=, .clear(), .truncate(), etc.
4. Converting Between Them (Very Common)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
fn main() { let owned = String::from("Hyderabad"); // String → &str (cheap, no allocation) let slice: &str = &owned; // or owned.as_str() println!("Slice: {}", slice); // &str → String (allocates new heap memory) let new_owned = slice.to_string(); // or String::from(slice) println!("New owned: {}", new_owned); } |
Rule of thumb in 2026 Rust:
- Prefer &str for function parameters (most flexible)
- Return String when creating new/modified text
- Return &str when just viewing existing data
5. Function Parameters — Why &str Wins Almost Always
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
fn print_city(city: &str) { // accepts &str, &String, String (coerces) println!("City: {}", city); } fn main() { let literal = "Hyderabad"; // &str let owned = String::from("Secunderabad"); print_city(literal); // OK print_city(&owned); // OK — &String coerces to &str print_city(&owned[..]); // explicit slice print_city(&owned.as_str()); // explicit } |
- &str accepts: &str, &String, String (via deref coercion)
- String only accepts String (moves) or &String if you change signature
→ Use &str in params → more ergonomic, no unnecessary clones
6. Mutating Strings (Only String or &mut str)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
fn main() { let mut s = String::from("Hello"); s.push_str(", world!"); s.replace_range(0..5, "Namaste"); // advanced mutation println!("{}", s); // Namaste, world! // &mut str — rare, but possible let mut slice: &mut str = &mut s[0..5]; // unsafe { slice.make_ascii_uppercase(); } // need unsafe for some ops } |
Most mutation happens on String.
7. Common Operations & Gotchas
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
fn main() { let text = "హైదరాబాద్"; // Telugu works perfectly (UTF-8) println!("Length (bytes): {}", text.len()); // 30 (bytes) println!("Chars count: {}", text.chars().count()); // 10 (Unicode chars) // Indexing: NO text[0] ! (because UTF-8 variable width) // Use .chars(), .bytes(), or slicing carefully let first_char = text.chars().next().unwrap(); println!("First char: {}", first_char); // హ } |
- .len() = bytes
- .chars().count() = Unicode scalar values
- No direct indexing text[0] — prevents invalid UTF-8 slices
8. Quick Best Practices (2026 Style)
- Function params → &str (flexible, zero-cost)
- Struct fields that own text → String
- Return new/modified text → String
- Return view of existing → &str (with lifetime)
- Avoid &String in params — prefer &str
- Use .to_string() or format! when building
- For performance → prefer &str + Cow<str> in advanced cases
Practice Project
|
0 1 2 3 4 5 6 7 |
cargo new rust_strings cd rust_strings |
Try in main.rs:
- Function that takes &str and returns String with ” – Hyderabad” appended
- Count Unicode chars in a Telugu sentence
- Build a greeting using String::new() + .push_str + .push emoji
Want me to explain any part deeper?
- UTF-8 details + why no indexing?
- String formatting (format!, println! tricks)?
- Cow<str> (cheap owned-or-borrowed)?
- Or next topic like structs or ownership deep dive with strings?
Just say — class continues! 🦀🚀
