I can think of several reasons:
- If you calculate the index, you can easily calculate a negative index by mistake. This would not be a memory-safety issue in Rust, but bounds-checking would no longer work reliably. This would make a common class of bugs impossible to detect at run-time.
- It would have a run-time cost (it needs an addition, at least, compared to bounds-checking). It would be generating machine code that isn't needed in most cases, hurting performance.
- Arrays may have a known size at compile-time, reducing the cost, but slices do not. Surely you'd want indexing to mean the same for both slices and arrays.
arr[-1]implies that the index is signed, so if you have a pointer you can use indexing only to access half of the possible memory range. This can be a problem in systems programming where the highest memory bit may have a special meaning.- Also, in C++ mixing signed and unsigned integers has a long history of pain and bugs. I'm not sure if this reasoning applies here, but at least many would feel better to avoid mixing signed and unsigned types whenever possible. (E.g. it requires care to compare those types with correct overflow handling.)
I can think of several reasons:
- If you calculate the index, you can easily calculate a negative index by mistake. This would not be a memory-safety issue in Rust, but bounds-checking would no longer work reliably. This would make a common class of bugs impossible to detect at run-time.
- It would have a run-time cost (it needs an addition, at least, compared to bounds-checking). It would be generating machine code that isn't needed in most cases, hurting performance.
- Arrays may have a known size at compile-time, reducing the cost, but slices do not. Surely you'd want indexing to mean the same for both slices and arrays.
arr[-1]implies that the index is signed, so if you have a pointer you can use indexing only to access half of the possible memory range. This can be a problem in systems programming where the highest memory bit may have a special meaning.- Also, in C++ mixing signed and unsigned integers has a long history of pain and bugs. I'm not sure if this reasoning applies here, but at least many would feel better to avoid mixing signed and unsigned types whenever possible. (E.g. it requires care to compare those types with correct overflow handling.)
You can easily implement it yourself if you want to.
use std::ops::{Index, IndexMut};
pub struct ReverseIndex(pub usize);
impl<T> Index<ReverseIndex> for [T] {
type Output = T;
fn index(&self, idx: ReverseIndex)->&T{
let len = self.len();
&self[len - 1 - idx.0]
}
}
impl<T> IndexMut<ReverseIndex> for [T] {
fn index_mut(&mut self, idx: ReverseIndex)->&mut T{
let len = self.len();
&mut self[len - 1 - idx.0]
}
}
fn main(){
let g = [1, 2, 3, 4, 5];
assert_eq!(g[ReverseIndex(1)], 4);
}
Like @Aplet123 answered, you may use casts. But if b is always negative, you could save the absolute value and just substact it instead:
let b = 1;
return a[SIZE/2 - b];
Cast it to an isize (a signed integer with the same size as a usize) first:
a[((SIZE / 2) as isize + b) as usize]
For example
fn main() { let x: u16 = 0; let mut v: Vec<u16> = Vec::new(); v.push(1); let y = v[x]; println!("{y}"); }
Doesn’t run but
fn main() { let x: u16 = 0; let mut v: Vec<u16> = Vec::new(); v.push(1); let y = v[x as usize]; println!("{y}"); }
Does
Yes, indexing into a string is not available in Rust. The reason for this is that Rust strings are saved in a contiguous UTF-8 encoded buffer internally, so the concept of indexing itself would be ambiguous, and people would misuse it: byte indexing is fast, but almost always incorrect (when your text contains non-ASCII symbols, byte indexing may leave you inside a character / unicode code point, which is really bad if you need text processing), while code point indexing is not free because UTF-8 is a variable-length encoding, so you have to traverse the entire string buffer to find the required code point.
If you are certain that your strings contain ASCII characters only, you can use the as_bytes() method on &str which returns a byte slice, and then index into this slice:
let num_string = num.to_string();
// ...
let b: u8 = num_string.as_bytes()[i];
let c: char = b as char; // if you need to get the character as a unicode code point
If you do need to index code points, you have to use the chars() iterator:
num_string.chars().nth(i).unwrap()
As I said above, this would require traversing the entire iterator up to the ith code element.
Finally, in many cases of text processing, it is actually necessary to work with grapheme clusters rather than with code points or bytes. For example, many emojis are composed of multiple code points, but are perceived as one "character". With the help of the unicode-segmentation crate, you can index into grapheme clusters as well:
use unicode_segmentation::UnicodeSegmentation
let string: String = ...;
UnicodeSegmentation::graphemes(&string, true).nth(i).unwrap()
Naturally, grapheme cluster indexing into the contiguous UTF-8 buffer has the same requirement of traversing the entire string as indexing into code points.
The correct approach to doing this sort of thing in Rust is not indexing but iteration. The main problem here is that Rust's strings are encoded in UTF-8, a variable-length encoding for Unicode characters. Being variable in length, the memory position of the nth character can't determined without looking at the string. This also means that accessing the nth character has a runtime of O(n)!
In this special case, you can iterate over the bytes, because your string is known to only contain the characters 0–9 (iterating over the characters is the more general solution but is a little less efficient).
Here is some idiomatic code to achieve this (playground):
fn is_palindrome(num: u64) -> bool {
let num_string = num.to_string();
let half = num_string.len() / 2;
num_string.bytes().take(half).eq(num_string.bytes().rev().take(half))
}
We go through the bytes in the string both forwards (num_string.bytes().take(half)) and backwards (num_string.bytes().rev().take(half)) simultaneously; the .take(half) part is there to halve the amount of work done. We then simply compare one iterator to the other one to ensure at each step that the nth and nth last bytes are equivalent; if they are, it returns true; if not, false.