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.Stringis an owned, growable buffer of text. It owns its bytes, so it can be extended, and aStringbound withlet mutcan 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&strto aString, andpush(ch)appends onechar. Both need the variable to bemut.- The
+operator joins aStringon the left with a&stron the right. It takes ownership of the left side, so the originalStringcan't be used afterwards unless youclone()it first. format!("Hello {}", name)builds a newStringfrom 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 withrev()orenumerate();collect()to build aStringor aVecfrom an iterator;push,push_strandformat!to build results;split_whitespace()andjointo 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.
&stris a borrowed read-only view,Stringis an owned, growable buffer.len()counts bytes. Usechars().count()for characters.- You can't write
s[i]. Usechars().nth(i), which returns anOption. - Slice ranges are byte positions and panic on a bad boundary or past the end.
- Use
push_str,pushandformat!to build strings, andcollect()to turn characters back into one.
