Rust String vs str

Rust has two string types that confuse many beginners: String and &str. Both hold text, but they work differently. Knowing when to use each one makes your code correct and efficient.

The Two String Types

String — Owned, Heap-Allocated

String is a growable text value stored on the heap. You own it, and you can add or remove characters from it.

let mut s = String::from("hello");
s.push_str(", world");   ← Append text
s.push('!');             ← Append a single character
println!("{}", s);       ← hello, world!

&str — Borrowed String Slice

&str is a reference to a sequence of characters stored somewhere else — in the program binary or inside a String on the heap. You do not own it and cannot change it through this reference.

let literal: &str = "hello";   ← Points to text in the binary

The Billboard Diagram

String   =  A whiteboard you own. You can write, erase, and rewrite.
&str     =  A photo of a billboard. You can read it but cannot change the original.

Where Each Type Lives

Type      Storage    Owned    Mutable    Grows/Shrinks
----      -------    -----    -------    -------------
String    Heap       Yes      Yes        Yes
&str      Anywhere   No       No         No

String Literals Are &str

Every string literal you write in code is a &str. It points directly into the compiled binary and is valid for the entire program's lifetime:

let a = "hello";        ← Type: &str (string literal)
let b = String::from("hello");  ← Type: String (heap-allocated copy)

Converting Between the Two Types

String to &str

let owned = String::from("hello");
let borrowed: &str = &owned;        ← Borrow a slice of the String
let also_borrowed = owned.as_str(); ← Explicit conversion

&str to String

let slice: &str = "hello";
let owned = slice.to_string();          ← Convert to String
let also_owned = String::from(slice);   ← Another way

The Photocopy Diagram

&str  ──to_string()──→  String     (make a copy you own)
String  ──&──────────→  &str       (borrow a view of it)

Which Type to Use in Function Parameters

Functions that only read text should accept &str. This works with both String values and string literals, making the function more flexible:

fn print_greeting(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let owned = String::from("Alice");

    print_greeting(&owned);     ← Pass a &String → coerces to &str automatically
    print_greeting("Bob");      ← Pass a literal &str directly
}

Functions that need to own the text, modify it, or store it should accept String.

Common String Operations

Concatenation

let s1 = String::from("hello");
let s2 = String::from(", world");

let s3 = s1 + &s2;   ← s1 is moved here; s2 is borrowed
                     ← s1 can no longer be used
println!("{}", s3);  ← hello, world

The + operator moves the first String and appends the borrowed second. For joining many strings, the format! macro is cleaner:

let result = format!("{} {}", "hello", "world");

Length and Indexing

let s = String::from("hello");
println!("Length: {}", s.len());   ← 5 bytes

Rust measures string length in bytes, not characters. For text with non-ASCII characters, use .chars().count() instead.

Checking Content

let s = "hello world";
println!("{}", s.contains("world"));     ← true
println!("{}", s.starts_with("hello")); ← true
println!("{}", s.ends_with("world"));   ← true

Splitting and Trimming

let csv = "alice,bob,carol";
for name in csv.split(',') {
    println!("{}", name);
}

let padded = "  hello  ";
println!("{}", padded.trim());   ← "hello"

String Indexing Warning

You cannot index a Rust string with s[0]. Strings are stored as UTF-8 bytes, and a single character can take 1 to 4 bytes. Indexing by byte position could split a character in half. Use slices only when you know the exact byte boundaries, or use .chars() to iterate character by character:

for c in "hello".chars() {
    println!("{}", c);
}

Quick Reference

String::from("text")     ← Create an owned String
"text"                   ← String literal (&str)
s.push_str("more")       ← Append to a String
s.len()                  ← Length in bytes
s.contains("x")          ← Check for substring
s.trim()                 ← Remove leading/trailing whitespace
s.split(',')             ← Split into parts
&owned_string            ← Borrow String as &str
slice.to_string()        ← Convert &str to String

Leave a Comment