What this lesson covers
Rust has a growable, owned string type, String, and a borrowed view of text, &str. A literal in double quotes, such as "Hello", is a &str. Text is stored as UTF-8 bytes, which is why Rust handles characters, lengths and positions differently from languages where a string is an array of characters.
The exercise turns "Java" into "TypeScript" using the main string tools: push_str, slicing, format! and chars() with collect().
Creating strings
let greeting = "Hello"; // &str, can't grow
let mut lang = "Java".to_string(); // String, can grow
let other = String::from("Rust"); // also a String
A &str is read-only. To get text you can change, make a String with to_string() or String::from, and declare the variable with mut.
Reading characters
Writing s[0] on a string doesn't compile in Rust. Ask the character iterator instead:
let s = "Hello";
let first = s.chars().nth(0).unwrap(); // 'H'
let last = s.chars().last().unwrap(); // 'o'
Both nth and last return an Option<char>, because the string might be too short or empty. unwrap() takes the character out and panics on None. For an empty string, "".chars().last() is None.
The len() method returns the length in bytes. For plain ASCII text, such as the words in this lesson, bytes and characters are the same, but a character like é takes two bytes, so "héllo".len() is 6 while "héllo".chars().count() is 5.
Joining strings
format! builds a new String from pieces, with each {} replaced by the next argument. push_str appends to a String in place:
let name = "John";
let str = format!("Hello {}", name); // "Hello John"
let mut greet = "Welcome".to_string();
greet.push_str(" to the course, ");
greet.push_str(name); // "Welcome to the course, John"
Slicing
A range inside square brackets takes part of a string. &s[start..end] starts at start and stops before end, and leaving the end out runs to the end of the string:
let s = "JavaScript";
println!("{}", &s[2..6]); // vaSc
println!("{}", &s[4..]); // Script
The leading & matters. The slice is a borrowed view into the original text, with no copy. The numbers are byte positions. Slicing panics if a number is past the end of the string or lands inside a multi-byte character. With ASCII text neither problem comes up as long as the range is inside the string.
Looping over characters
let my_str = "Andy";
for c in my_str.chars() {
println!("{}", c);
}
for (i, c) in my_str.chars().enumerate() {
println!("{} {}", i, c);
}
The first loop prints A, n, d and y on separate lines. The second also gives each character its position: 0 A, 1 n, and so on. If the loop body doesn't use i, the compiler warns about an unused variable, but the code still runs. Both loops take O(n) time for a string of n characters.
The exercise, step by step
- Make an owned, mutable string.
"Java".to_string()creates aStringfrom the literal, andlet mut langallows changes. - Append the rest of the word.
lang.push_str("script")changeslangin place so that it holds"Javascript". - Take a slice.
&lang[4..]is the text from position 4 to the end,"script". It borrows fromlang. - Join with
format!.format!("Type{}", substring)produces the newString"Typescript". This variable is alsomut, because the last step reassigns it. - Replace a character. Since a
Stringcan't be edited by position, collect its characters into aVec<char>, set the element at index 4 to'S', and collect the characters back into aString. The result is"TypeScript".
The full solution
let mut lang = "Java".to_string();
lang.push_str("script");
let substring = &lang[4..];
let mut new_lang = format!("Type{}", substring);
let mut characters: Vec<char> = new_lang.chars().collect();
characters[4] = 'S';
new_lang = characters.iter().collect();
After these lines, substring is "script" and new_lang is "TypeScript".
collect()needs a target type. The annotationVec<char>oncharacterstells the firstcollect()what to build. For the second, the target isnew_lang, which is already aString, so the same call builds aStringthis time.iter()yields references.characters.iter()produces&charvalues, andStringcan be built from those as well as from plaincharvalues.- Single quotes mean
char.'S'is a single character and"S"is a string. Assigning"S"to aVec<char>element is a type error. - The slice borrows
lang. Whilesubstringis in use,langmust not be changed. Heresubstringis last used insideformat!, so nothing conflicts.
Common mistakes
- Indexing a string.
lang[4]is a compile error. Usechars().nth(4), or a slice such as&lang[4..5]. - Forgetting
mut.push_stron a variable declared withoutmutdoesn't compile. - Calling
push_stron a literal. A&strcan't grow. Convert it to aStringfirst. - Dropping the
&in a slice.lang[4..]alone has no known size, so the compiler rejects it. Write&lang[4..]. - Off-by-one in a range. The end index is excluded, so
&s[2..6]gives four characters. - Using byte positions with non-ASCII text. A slice that cuts a multi-byte character in half panics.
Key takeaways
Stringis owned and growable, and&stris a borrowed view.- Read characters with
chars(),nthandlast, which returnOption. - Build text with
push_strandformat!. - Slice ranges use byte positions and exclude the end index.
- To change a character, collect into a
Vec<char>, edit it and collect back.
