Start learning free

RUST · STRINGS

Intro To Strings in Rust, explained

What a string is

A string is a sequence of characters used for text: names, messages, words, whole sentences. Many interview problems take a string as input, from reversing it to checking whether it reads the same backwards, so it is worth learning how Rust treats strings before you start solving them.

Rust is stricter about strings than most languages. The strictness comes from one decision: Rust strings are always valid UTF-8 text. UTF-8 stores each character in one to four bytes. Plain English letters, digits and common punctuation take one byte each, while characters such as é take two and many symbols take more. Most of what looks unusual about Rust strings follows from this.

Two string types

Rust has two main string types, and you will use both constantly.

  • &str (a "string slice") is a borrowed view of text that lives somewhere else. A string literal such as "Hello" has this type. You can read it, but you can't grow it or change it.
  • String is an owned, growable buffer of text. It owns its bytes, so it can be extended, and a String bound with let mut can be changed in place.
let greeting = "Hello";                 // &str
let mut owned = String::from("Hello");  // String
owned.push_str(", world");
println!("{}", owned); // Hello, world

Functions in this course often take a String parameter and return a String. Inside them, you borrow pieces of it as &str. You can also turn a literal into a String with String::from("text") or "text".to_string().

Length is counted in bytes

The len() method returns the number of bytes, not characters. For plain ASCII text the two are the same. For other text they are not:

let word = "héllo";
println!("{}", word.len());           // 6
println!("{}", word.chars().count()); // 5

To count characters, go through the chars() iterator. It walks the string one Unicode character at a time, and count() tallies them.

No indexing by position

In many languages you write s[0] to get the first character. In Rust this doesn't compile for strings. A byte position and a character position are different things in UTF-8, and a simple s[i] would have to either lie or be slow, so Rust leaves it out. Use the character iterator instead:

let s = "Hello, world";
println!("{:?}", s.chars().nth(1));  // Some('e')
println!("{:?}", s.chars().nth(50)); // None

nth(i) returns an Option<char>: Some(character) when the position exists and None when the string is too short. Calling unwrap() on it gives the character but panics on None. Finding the n-th character this way takes O(n) time, because the iterator has to walk past every character before it.

Slicing uses byte positions

You can cut out part of a string with a range: &s[start..end] includes start and stops before end. The numbers are byte positions:

let u = String::from("Hello, world");
let first = &u[0..5];
println!("{}", first); // Hello

Slicing panics in two situations. One is a range that runs past the end of the string. The other is a range that starts or ends in the middle of a multi-byte character. For "héllo", the slice [0..2] stops inside é, which occupies bytes 1 to 2, and the program stops with a message that the byte index is not a character boundary. With ASCII-only text every byte is a full character, so this never happens there.

Building and changing strings

  • push_str(text) appends a &str to a String, and push(ch) appends one char. Both need the variable to be mut.
  • The + operator joins a String on the left with a &str on the right. It takes ownership of the left side, so the original String can't be used afterwards unless you clone() it first.
  • format!("Hello {}", name) builds a new String from pieces and doesn't take ownership of anything it reads. It is often the easiest way to join text.
let t = String::from("Hello");
let u = t.clone() + ", " + "world";
let v = format!("{} and {}", t, u);
println!("{}", v); // Hello and Hello, world

A String can't be edited character by character. To change one character, collect the characters into a Vec<char>, change the vector, then collect it back into a String. The same collect() idea reverses a string: s.chars().rev().collect::<String>() gives "cba" for "abc".

Comparing strings

The == operator compares contents, so "abc" == String::from("abc") is true. Two strings are equal when they have the same characters in the same order, and the comparison is case-sensitive.

How this fits the chapter

The problems in this chapter ask you to reverse strings, check palindromes and rearrange words. You will lean on a small set of tools:

  • chars() to walk through characters, often with rev() or enumerate();
  • collect() to build a String or a Vec from an iterator;
  • push, push_str and format! to build results;
  • split_whitespace() and join to break text into words and put it back together.

Walking a whole string always takes O(n) time for a string of length n. Keep an eye on operations that look cheap but aren't, such as inserting at the front of a String, which shifts every byte already there.

Key takeaways

  • Rust strings are UTF-8: one character can take one to four bytes.
  • &str is a borrowed read-only view, String is an owned, growable buffer.
  • len() counts bytes. Use chars().count() for characters.
  • You can't write s[i]. Use chars().nth(i), which returns an Option.
  • Slice ranges are byte positions and panic on a bad boundary or past the end.
  • Use push_str, push and format! to build strings, and collect() to turn characters back into one.
Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
Start learning free