Site

Strings — String and &str

Tutorial S1.0  •  Rust / Learn / StdLib

S1.0 What This Teaches

Rust has two string types that serve different purposes. Understanding both - and when to use each - is essential for almost every Rust program. This tutorial covers:

S1.1 Two String Types

&str (string slice) is a reference to a sequence of UTF-8 bytes stored somewhere else - usually in the program's read-only binary or in a String on the heap. It is borrowed: you cannot grow it or own it. String is an owned, heap-allocated, growable buffer of UTF-8 bytes. You can append to it, modify it, and pass ownership of it.
let s: &str   = "hello";          // lives in read-only memory; borrowed
let t: String = String::from(s);  // heap-allocated copy; owned
Function parameters that only need to read a string should take &str - it accepts both &str literals and &String via deref coercion. Parameters that need to own or grow the string take String.

S1.2 Constructing a String

let a = String::from("Rust");
let b = "world".to_string();
let c = format!("{a} {b}");   // format! never moves its arguments
println!("{c}");               // Rust world
format! is the most flexible constructor - it composes values of any type without allocating intermediate strings, and does not move any of its arguments.

S1.3 Concatenation

The + operator appends a &str to a String. It moves the left operand, so the original binding becomes invalid afterward:
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;   // s1 is moved; s2 is borrowed
// s1 is no longer usable here
println!("{s3}");     // Hello, world!
When combining many strings, prefer format! - it does not move any of its arguments and produces a fresh String.

S1.4 Query Methods

let msg = String::from("  Hello, Rust!  ");
println!("{}", msg.len());                       // 16 (byte count, not char count)
println!("{}", msg.contains("Rust"));            // true
println!("{}", msg.trim().starts_with("Hello")); // true
println!("{}", msg.trim().ends_with("!"));       // true
len() returns byte length. For ASCII strings that equals character count, but multi-byte Unicode characters cause them to differ. Use .chars().count() for character count.

S1.5 Transformation Methods

Transformation methods return new values - they do not modify the original string:
let msg = "  Hello, Rust!  ";
println!("{}", msg.trim());                          // "Hello, Rust!"
println!("{}", msg.trim().to_uppercase());           // "HELLO, RUST!"
println!("{}", msg.trim().to_lowercase());           // "hello, rust!"
println!("{}", msg.trim().replace("Rust", "World")); // "Hello, World!"

S1.6 Splitting

split returns a lazy iterator of &str slices. Call .collect() to gather them into a Vec:
let csv = "one,two,three,four";
let parts: Vec<&str> = csv.split(',').collect();
println!("{parts:?}");   // ["one", "two", "three", "four"]
Other useful splitting methods: split_whitespace(), splitn(n, pat), lines().

S1.7 Characters

Rust strings are UTF-8. Indexing by byte position is not always meaningful, so Rust does not allow s[0] on a string. Instead, iterate over Unicode scalar values with .chars():
for ch in "Rust".chars() { print!("{ch} "); }
println!();
println!("{}", "Rust".chars().count());   // 4
Because .chars() returns an iterator, all iterator adaptors (filter, map, enumerate, ...) apply directly.

S1.8 Conversion and Parsing

&String coerces to &str automatically (deref coercion) - no copy occurs, just a reference into the String's buffer:
let owned: String  = String::from("owned");
let borrowed: &str = &owned;   // reference into owned's buffer
println!("{borrowed}");
Parse a &str into a numeric type with .parse(). It returns Result - use ? in a function that propagates errors, or .unwrap() when failure cannot happen:
let n: i32 = "42".parse().unwrap();
println!("{n}");   // 42

S1.9 Example - All Together

// Strings - demonstrates String vs &str, construction, common methods, and conversion.

fn main() {
    // --- String vs &str ---
    println!("--- String vs &str ---");
    let s: &str   = "hello";
    let t: String = String::from(s);
    println!("&str: {s}, String: {t}");

    // --- construction ---
    println!("--- construction ---");
    let a = String::from("Rust");
    let b = "world".to_string();
    let c = format!("{a} {b}");
    println!("{c}");

    // --- concatenation with + moves the left operand ---
    println!("--- concatenation ---");
    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2;
    println!("{s3}");

    // --- common query methods ---
    println!("--- query methods ---");
    let msg = String::from("  Hello, Rust!  ");
    println!("len:         {}", msg.len());
    println!("contains:    {}", msg.contains("Rust"));
    println!("starts_with: {}", msg.trim().starts_with("Hello"));
    println!("ends_with:   {}", msg.trim().ends_with("!"));

    // --- transformation ---
    println!("--- transform ---");
    println!("trim:         '{}'", msg.trim());
    println!("to_uppercase: {}", msg.trim().to_uppercase());
    println!("to_lowercase: {}", msg.trim().to_lowercase());
    println!("replace:      {}", msg.trim().replace("Rust", "World"));

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

    // --- chars: iterate over Unicode scalar values ---
    println!("--- chars ---");
    let word = "Rust";
    for ch in word.chars() { print!("{ch} "); }
    println!();
    println!("char count: {}", word.chars().count());

    // --- deref coercion: &String -> &str ---
    println!("--- conversion ---");
    let owned   = String::from("owned");
    let borrowed: &str = &owned;
    println!("borrowed: {borrowed}");

    // --- parse: &str to numeric type ---
    println!("--- parse ---");
    let n: i32 = "42".parse().unwrap();
    println!("parsed: {n}");
}
Expected output:
--- String vs &str ---
&str: hello, String: hello
--- construction ---
Rust world
--- concatenation ---
Hello, world!
--- query methods ---
len:         16
contains:    true
starts_with: true
ends_with:   true
--- transform ---
trim:         'Hello, Rust!'
to_uppercase: HELLO, RUST!
to_lowercase: hello, rust!
replace:      Hello, World!
--- split ---
["one", "two", "three", "four"]
--- chars ---
R u s t
char count: 4
--- conversion ---
borrowed: owned
--- parse ---
parsed: 42

S1.10 Exercise

Exercise
  • Write a function word_count(s: &str) -> usize that returns the number of whitespace-delimited words. Test it on a sentence.
  • Write a function capitalize(s: &str) -> String that returns a new String with the first character uppercased and the rest unchanged. Use .chars() to get the first character and &s[1..] for the remainder.
  • Split "2024-07-09" on '-' and parse each part to u32. Print year, month, and day on separate lines.

S1.11 Common Mistakes

Indexing a String with []

let s = String::from("hello");
let c = s[0];   // error: String cannot be indexed by integer
Use .chars().nth(0) for a character, or &s[0..1] for a byte slice (safe only for ASCII).

Treating len() as character count

println!("{}", "café".len());          // 5 (bytes)
println!("{}", "café".chars().count()); // 4 (characters)
len() counts bytes. The é character encodes to two bytes in UTF-8, so byte count and character count differ for non-ASCII strings.

Moving a String into + unexpectedly

let s1 = String::from("a");
let s2 = String::from("b");
let s3 = s1 + &s2;
println!("{s1}");   // error: s1 was moved into +
Use format!("{s1}{s2}") to avoid moving either operand.

Passing String where &str is expected (or vice versa)

fn greet(name: String) { println!("Hello, {name}"); }

greet("Alice");              // error: expected String, found &str
greet("Alice".to_string());  // ok
greet(String::from("Alice")); // ok
Prefer &str parameters in function signatures - they accept both string literals and &String without conversion.

S1.12 Key Terms

TermMeaning
&strBorrowed string slice; reference to UTF-8 bytes stored elsewhere; cannot be grown
StringOwned, heap-allocated, growable UTF-8 string buffer
deref coercionAutomatic conversion of &String to &str by the compiler
format!Macro that builds a String from a format template without moving arguments
.chars()Iterator over Unicode scalar values (char), not raw bytes
.len()Returns byte length of the string, which may differ from character count for Unicode
.parse()Converts a &str to another type; returns Result
.split()Returns a lazy iterator of &str slices divided by a pattern
.trim()Returns a &str with leading and trailing whitespace removed
.replace()Returns a new String with all occurrences of a pattern substituted