While the other answer explains how to understand and remember the indexing behavior implemented in Rust standard library, the real reason why it is the way it is has nothing to do with technical limitations. It comes down to the design decision made by the authors of Rust standard library.
Given v = vec![1,2,3,4], why does v[4..] return an empty vector, but v[5..] panics [..] ?
Because it was decided so. The code below that handles slice indexing (full source) will panic if the start index is larger than the slice's length.
fn index(self, slice: &[T]) -> &[T] {
if self.start > slice.len() {
slice_start_index_len_fail(self.start, slice.len());
}
// SAFETY: `self` is checked to be valid and in bounds above.
unsafe { &*self.get_unchecked(slice) }
}
fn slice_start_index_len_fail(index: usize, len: usize) -> ! {
panic!("range start index {} out of range for slice of length {}", index, len);
}
How could it be implemented differently? I personally like how Python does it.
v = [1, 2, 3, 4]
a = v[4] # -> Raises an exception - Similar to Rust's behavior (panic)
b = v[5] # -> Same, raises an exception - Also similar to Rust's
# (Equivalent to Rust's v[4..])
w = v[4:] # -> Returns an empty list - Similar to Rust's
x = v[5:] # -> Also returns an empty list - Different from Rust's, which panics
Python's approach is not necessarily better than Rust's, because there's always a trade-off. Python's approach is more convenient (there's no need to check if a start index is not greater than the length), but if there's a bug, it's harder to find because it doesn't fail early.
Although Rust can technically follow Python's approach, its designers decided to fail early by panicking in order that a bug can be faster to find, but with a cost of some inconvenience (programmers need to ensure that a start index is not greater than the length).