10.4 Slice Indexing and Range Syntax
Right, let’s talk about slicing the slice. It’s a bit meta, but it’s also where you’ll spend a lot of your time and, consequently, where you’ll meet the dreaded panic! if you’re not careful. I’m going to show you how to avoid that fate.
Think of a slice as a window into some data. Indexing is how you point to a specific seat in the row of data your window is looking at. You use the index operator, [], with a single usize value.
let my_favorite_words = ["hello", "cruel", "world"];
let word_slice = &my_favorite_words[..]; // A slice of the whole array
let first_word = word_slice[0]; // "hello"
let second_word = word_slice[1]; // "cruel"
Seems simple, right? It is. Until you ask for an index that doesn’t exist. This isn’t one of those languages where it’ll gently hand you back undefined or null and let you stumble on until your program does something weird three hours later. Rust stops the show immediately. It panics. It’s the language’s way of saying, “Absolutely not. I know what you’re trying to do, and I refuse to let you do it incorrectly.”
// This will cause a runtime panic. Don't actually run this.
// let panic_time = word_slice[5]; // index out of bounds: the len is 3 but the index is 5
This is a feature, not a bug. Memory safety isn’t negotiable. Accessing invalid memory is the root of so many security vulnerabilities, and Rust simply won’t have it. So you, the programmer, have to ensure your indices are always in bounds. This often means checking the length (slice.len()) before you index, especially when the index comes from user input or another external source.
The Range Syntax: Your Window Within a Window
This is where it gets more interesting. Instead of a single index, you can provide a range within the [] to get a sub-slice—a smaller window looking at a part of the original data. The range syntax is brilliantly concise, if occasionally a bit confusing at first glance.
There are a few flavors, and you need to know them all:
let numbers = [0, 1, 2, 3, 4, 5];
let num_slice = &numbers[..];
// RangeFull: Just the `..` - it takes the whole slice.
let all = &num_slice[..]; // [0, 1, 2, 3, 4, 5]
// RangeFrom: `start..` - from index `start` to the end.
let from_two = &num_slice[2..]; // [2, 3, 4, 5]
// RangeTo: `..end` - from the start *up to, but not including*, index `end`.
let up_to_four = &num_slice[..4]; // [0, 1, 2, 3]
// Range: `start..end` - from `start` to, but not including, `end`.
let middle = &num_slice[2..5]; // [2, 3, 4]
// RangeInclusive: `start..=end` - from `start` to, and including, `end`.
// (Note the `=` in the syntax)
let inclusive_middle = &num_slice[2..=4]; // [2, 3, 4]
The most common mistake here is an off-by-one error with the exclusive range (..). Remember, the end index is like the number you’d use in a for loop: for i in 0..5—i will be 0, 1, 2, 3, 4. It stops at 5; it doesn’t execute for 5. The slice range [0..5] behaves exactly the same way.
The One Rule of Range Club
The rules for bounds-checking are different here. While a single index must be less than the length, a range’s end index can be equal to the length. Why? Because that’s a perfectly valid way to say “go until the end.” The rule is that the start index must be <= the end index, and the end index must be <= the len.
These are all valid, even though 6 is beyond the final element’s index:
let valid1 = &num_slice[3..6]; // [3, 4, 5] - end is equal to len, okay!
let valid2 = &num_slice[6..]; // [] - an empty slice starting at len
let valid3 = &num_slice[6..6]; // [] - an empty slice
But these will panic:
// Panic! start (7) is greater than end (6)
// let panic_range = &num_slice[7..6];
// Panic! start (3) is greater than end (2)
// let another_panic = &num_slice[3..2];
The logic is sound. An empty slice at the very end of your data is a coherent, safe concept. Asking for a slice where the start is after the end is fundamentally nonsensical, and Rust calls you out on it.
Best Practices and the Almighty get Method
Now, constantly checking len() yourself before every slice operation is a pain. It’s verbose and clutters your logic. This is where the slice’s get method becomes your best friend.
Instead of using [], you can use get(index) which returns an Option<&T>. If the index is valid, it’s Some(&value). If it’s invalid, it’s None. No panic. Just a gentle, Rust-y nudge to handle the error case.
let safe_word = word_slice.get(1); // Some(&"cruel")
let safe_but_silly = word_slice.get(5); // None
// You can use it with match for robust error handling
match word_slice.get(2) {
Some(word) => println!("The word is: {}", word),
None => println!("No word at that index, chief."),
}
// Or use `unwrap_or` for a quick default
let third_word = word_slice.get(2).unwrap_or(&"nothing");
There’s a corresponding get_mut for when you have a mutable slice, too. Use get when the index is potentially out-of-bounds (e.g., calculated dynamically, comes from input). Use the indexing operator [] when you, the programmer, can guarantee the index is valid because of your logic. It’s a signal of confidence to anyone reading your code.