I get your confusion :< the syntax for &var in is indeed confusing. It speaks like taking the memory address of each element and we need to dereference it afterwards, because the syntax & is the language of "taking the memory address" in Rust. But it suddently becomes "take the memory address and dereference it". So to avoid confusion like this, I think syntax should represent the last action. So it should be
for *name in data {
// it is clear, name is already value not memory address, because *name returns value
// so user clearly can know they can use it directly without dereference again, because the keyword `*` already communicate it is already value that is taken from a memory address, not it is still memory address
// user can clearly know they do not need to dereference (*) again
println!("{}", name);
}
Others have mentioned the technical detail, I want to provide a different perspective from high level explanation focusing on what is going on, not the implementation design
Let's start with your const variable. It is
const name = &[ &str ]
Which is a slice, aka a fat pointer, fat pointer is pointer that has runtime len metadata. The slice's pointer points to multiple T in contigous memory but only within certain range, the range is 0 until the value of len. Here is the illustration
struct name<T> {
ptr: *const T,
len: usize
}
It is different than &[Type; Length]. It has compile time length. Aka it doesn't has runtime len metadata. A pointer that doesn't has any runtime metadata is called thin pointer. The pointer points to fixed size contiguous memory. Here is the illustration
struct a<T> {
ptr: *const T
}
Since you created a const value, intended to never be mutated again, you should use thin pointer variant instead. Because it does not have runtime len metadata. Suprisingly the compiler doesn't optimize slices that are guaranteed never change again, like your code above
const ITEMS: &[&str] = &["ITEM_1", "ITEM_2"];
Despite it is declared as const, with literal values, I checked the assembly, it never be optimized to thin pointer. I will search does the two has different optimization capabilities first :v I will share the result of it :>
Now, let's move to the iterator thing
Your variable is
const ITEMS: &[&str] = &["ITEM_1", "ITEM_2"];
The values of your variable above are multiple &str. Remember &str is another pointer that points to a static data embedded in the binary. The &str is independent from the iterator, iterator only communicates directly with the container, aka the &[...], so you can ignore &str to avoid the confusion of double &&. Let's call it type instead. So your variable is &[type]
Let's talk about the normal for in first
for val in data
Let's call val is the left type, data is the right type
The default behavior of for in iterator is
-
if right type is value type + implement copy -> val becomes owned type that copy the element
-
if the right type is value type + does not implement copy -> val becomes owned type that move the element
-
if the right type is reference type like &[...], &Vec, etc -> val becomes reference that points to the element (borrow)
Additional :
.iter() make value types become reference types that can be used for reference based iterator. Like
let data = vec![1, 2, 3];
// .iter() makes owned value become reference that can be used for iteration without copying or moving the element
for val in data.iter() {
}
// you can still use data here
Now for &val in data just extends the reference based iterator above becomes iterator by reference + auto dereference. It can only be used in reference based iterators, because ofc no need to dereference in an already value type
So when you encounter
for &val in data.iter() {
let get = val;
}
Remember it is just a sugar for
for val in data.iter() {
let get = *val;
}
It removes the need of calling * (dereference) each time need to access the value
Or you can keep this synonym for easy understanding. for &val in data is just for *val in data. It auto dereference