String and UTF-8 text

Rust · Practical · 12 min

Text handling is where newcomers trip most, because Rust strings are always valid UTF-8 and never indexed by position.

Rust has two main string types. `String` is an owned, growable, heap-allocated buffer. `&str` is a borrowed slice of UTF-8 text, from a String or a literal. Use &str for parameters, String when you need to own or build text. Convert with s.as_str() / &s one way and s.to_string(), String::from(s) or s.to_owned() the other.

Building text: push_str, push (one char), + (which takes the left String by value: s1 + &s2), and the format! macro, which is usually the clearest. write! into a String works too via std::fmt::Write.

Both types are guaranteed valid UTF-8. A character may take 1 to 4 bytes, so:

  • s.len() is the length in bytes, not characters.
  • s[0] does not compile. There is no O(1) way to get the n-th character.
  • s.chars() iterates Unicode scalar values, s.bytes() iterates bytes, and s.char_indices() gives valid byte offsets for slicing.

Common operations: trim, split, split_whitespace, lines, contains, starts_with, strip_prefix, replace, to_lowercase, parse::<T>(), repeat, and chars().rev().collect::<String>() to reverse.

What users perceive as a character (a grapheme, e.g. an emoji with a skin-tone modifier) can be several chars; for that you need the unicode-segmentation crate.

Other string types exist for good reasons: OsString/OsStr for OS data that may not be UTF-8, PathBuf/Path for file paths, and CString/CStr for C interop. Do not force those through String.

Example

use std::fmt::Write;

fn initials(full_name: &str) -> String {
    full_name.split_whitespace().filter_map(|w| w.chars().next()).collect()
}

fn main() {
    let mut s = String::from("héllo");
    s.push_str(", wörld");
    s.push('!');
    println!("{} bytes, {} chars", s.len(), s.chars().count()); // 15 bytes, 13 chars

    let greeting = format!("{} ({})", s.to_uppercase(), initials("Ada King Lovelace"));
    println!("{greeting}"); // HÉLLO, WÖRLD! (AKL)

    let mut report = String::new();
    for (i, line) in "a=1\nb=2".lines().enumerate() {
        writeln!(report, "{i}: {line}").unwrap();
    }
    print!("{report}"); // 0: a=1 / 1: b=2

    let n: i64 = " 42 ".trim().parse().expect("a number");
    println!("{n} {:?}", s.chars().next()); // 42 Some('h')   (s[0] would not compile)
}

Remember

  • String owns text, &str borrows it; take &str parameters
  • len() counts bytes; iterate with chars() and slice at char_indices offsets
  • format! is the clearest way to build strings

Watch out for

  • Assuming s.len() is the number of characters for non-ASCII text.

Check yourself

  1. What is `"héllo".len()`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/book/ch08-02-strings.html

Builds on

Unlocks

Nothing builds on this yet.