Valid positions
The characters of a string sit at positions 0 to length - 1. The string "ford" has four, at 0, 1, 2 and 3. Position 4 is one past the end, and it is not valid. The most common slip with strings is asking for a position that does not exist, the "exceeding the bounds" error.
What happens next depends on how you ask for the character. Rust has three behaviours, and knowing which one you are getting is the point of this lesson.
1. The call returns None
chars().nth(i) returns an Option<char>. For a position inside the string it is Some(character), and for a position outside it is None. Nothing goes wrong yet:
let car = "ford";
println!("{}", car.chars().nth(3).unwrap());
println!("{:?}", car.chars().nth(4));
d
None
The panic comes from unwrap(), which stops the program when it is called on None. This line:
println!("{}", car.chars().nth(4).unwrap());
ends with the message called `Option::unwrap()` on a `None` value. The same applies to get(4..5) on the string, which returns None. Keeping the Option and handling it with match, if let or unwrap_or avoids the panic completely.
2. The program panics
Some operations don't return an Option. Slicing with a range, and indexing a Vec, check the bounds and panic if they are broken, with a message that names the problem:
let car = "ford";
let i = 5;
println!("{}", &car[i..]);
byte index 5 is out of bounds of `ford`
With a Vec<char> named v of length 4, v[i] for i = 4 panics with index out of bounds: the len is 4 but the index is 4. A panic ends the program immediately, instead of letting it continue with wrong data. That is a good thing: a loud failure at the mistake is much easier to debug than a wrong answer found later.
3. The result is quietly empty
The trap is a range that starts exactly at the length. A start equal to the length is allowed, because the range is empty, not outside the string:
let car = "ford";
println!("[{}]", &car[car.len()..]);
[]
There is no panic and no error, only an empty string. Only a start greater than the length panics. This is the bug in the lesson's exercise, and it is harder to spot than a crash, because the program runs to the end and prints something.
The lesson's exercise
The code should print the last character of message, which is s. It prints an empty line instead:
let message = "I love pancakes";
println!("{}", &message[message.len()..]);
The string has 15 bytes, at positions 0 to 14. The slice starts at position 15, one past the last character, so it is empty. The last character is at position 14, which is message.len() - 1. Moving the start back by one fixes the code:
let message = "I love pancakes";
println!("{}", &message[message.len() - 1..]);
This prints s. The slice now starts at the last character and runs to the end.
Defensive patterns
- Use the checked accessors when the position comes from input:
get(range)returnsNoneinstead of panicking, andchars().nth(i)does too. - Guard subtractions from the length. The length is unsigned, so on an empty string
message.len() - 1wraps to a huge number in an optimized build and panics in a debug build, and the slice after it panics too. Testis_empty()first. - Remember the boundary. A single-character slice
&s[i..i + 1]needsi + 1 <= s.len(), which meansimust be less than the length. - Beware of multi-byte characters. In text that is not ASCII, a byte position can fall in the middle of a character, which also panics.
Key takeaways
- Valid positions run from 0 to
len() - 1. nthandgetreturn anOption;unwrap()turns aNoneinto a panic.- Range slices and
Vecindexing panic when the bounds are broken. - A slice that starts at
len()is empty, not an error, so check that it starts at the right position.
