I am teaching myself Rust by reading through "the book". When I was working on the first exercise from this link: https://doc.rust-lang.org/book/ch08-03-hash-maps.html, I found getting the types correct with for loop was really tricky. I eventually got the code to work by trial and error, but I am really confused by how it works.
Here I simplified some of the issues I ran into and left my questions inline. I tried as many variations as I could but I think the main confusions are:
- the difference between
loop1
andloop2
(I believe 3 and 4, 5 and 6 are analogous) - The inline question in
loop1
andloop2
- What is the problem with
loop3_bad
?
fn main() {
// using mut because it is needed in the original code
let mut list = vec![10, 14, 10, 12, 9, -2, 14, 10, 14];
let ret1 = loop1(&list);
let ret2 = loop2(&list);
// let ret3 = loop3_bad(&mut list);
let ret4 = loop4(&mut list);
let ret5 = loop5(&mut list);
let ret6 = loop6(&mut list);
let ret7 = loop7(&mut list);
println!("loop1 ret={:?}", ret1);
println!("loop2 ret={:?}", ret2);
// println!("loop3 ret={:?}", ret3);
println!("loop4 ret={:?}", ret4);
println!("loop5 ret={:?}", ret5);
println!("loop6 ret={:?}", ret6);
println!("loop7 ret={:?}", ret7);
}
fn loop1(list: &Vec<i32>) -> Option<f64> {
if list.is_empty() {
None
} else {
let mut sum: f64 = 0.0;
for &i in list {
sum = f64::from(i);
// cannot write f64::from(*i)
// error would be:
// error[E0614]: type `i32` cannot be dereferenced
//
// How should I read the for syntax?
// Is it because "&i" of &i32 type, therefore i is of "i32" type?
// or should I treat "&i" a declaration that i is of "&i32" type?
}
Some(sum/list.len() as f64)
}
}
fn loop2(list: &Vec<i32>) -> Option<f64> {
if list.is_empty() {
None
} else {
let mut sum: f64 = 0.0;
for i in list {
sum = f64::from(*i);
// cannot write f64::from(i)
// error would be:
// the trait `From<&i32>` is not implemented for `f64`
//
// is i of "&i32" type?
// is the dereferencing required here?
}
Some(sum/list.len() as f64)
}
}
// This one causes compilation error, but why?
// If `list` is moved by the loop, why didn't this cause issue in loop1()?
//
// Rust ERROR:
//
// error[E0382]: borrow of moved value: `list`
// --> for_loop_example.rs:65:18
// |
// 57 | fn loop3(list: &mut Vec<i32>) -> Option<f64> {
// | ---- move occurs because `list` has type `&mut Vec<i32>`, which does not implement the `Copy` trait
// ...
// 62 | for &mut i in list {
// | ----
// | |
// | `list` moved due to this implicit call to `.into_iter()`
// | help: consider borrowing to avoid moving into the for loop: `&list`
// ...
// 65 | Some(sum/list.len() as f64)
// | ^^^^ value borrowed here after move
// |
// note: this function takes ownership of the receiver `self`, which moves `list`
//
// fn loop3_bad(list: &mut Vec<i32>) -> Option<f64> {
// if list.is_empty() {
// None
// } else {
// let mut sum: f64 = 0.0;
// for &mut i in list {
// sum = f64::from(i);
// }
// Some(sum/list.len() as f64)
// }
// }
fn loop4(list: &mut Vec<i32>) -> Option<f64> {
if list.is_empty() {
None
} else {
let mut sum: f64 = 0.0;
for i in &*list {
// what does &*list even do?
sum = f64::from(*i);
}
Some(sum/list.len() as f64)
}
}
fn loop5(list: &mut Vec<i32>) -> Option<f64> {
if list.is_empty() {
None
} else {
let mut sum: f64 = 0.0;
for &i in &*list { // similar to loop4, excpet for using &i
sum = f64::from(i);
}
Some(sum/list.len() as f64)
}
}
fn loop6(list: &mut Vec<i32>) -> Option<f64> {
if list.is_empty() {
None
} else {
let mut sum: f64 = 0.0;
for &mut i in &mut *list { // looking similar to loop5
sum = f64::from(i);
}
Some(sum/list.len() as f64)
}
}
fn loop7(list: &mut Vec<i32>) -> Option<f64> {
if list.is_empty() {
None
} else {
let mut sum: f64 = 0.0;
for i in &mut *list { // looking similar to loop4, except for using mut
sum = f64::from(*i);
}
Some(sum/list.len() as f64)
}
}
CodePudding user response:
There are few building blocks here.
When you iterate over &Vec
, you get back shared references to its contents. That is, iterating over &Vec<i32>
will give you elements of type &i32
. When you iterate over &mut Vec
, you get back mutable references - &mut i32
- that allow you to change, and not only inspect, the contents (also, when you iterate over Vec
you get owned i32
, but that is not relevant to our discussion).
The second thing important to know is that in Rust, there are actually two ways to dereference a reference: the first being the usual *
, the second is using patterns. When I bind a new variable,
let x = value;
x
is actually not just a name, but a pattern (an irrefutable one). I used identifier pattern that allows me to just bind the value to a name, but there are other kinds too. One of them is the reference pattern: &
or &mut
then a nested pattern. What it does is dereferencing the value and then binding the nested pattern to the dereferenced value. So, the following:
let &i = &value;
Is equivalent to let i = value;
(you can also see the symmetry, which is why this syntax was chosen).
Not just let
permits patterns: any binding in Rust does. Parameters, match
arguments, and also, for
loops.
Armed with this knowledge let's understand the loops one by one:
In loop1
, we're iterating over &Vec<i32>
. This gives us &i32
s. However, we're using &i
is the pattern - that means i
is only bound to the i32
part. Since this is not a reference you cannot dereference it anymore.
loop2
is the same but without the reference pattern. Here i
is &i32
, so you must dereference it.
loop3_bad
is interesting. It does the same of loop1
but with mutable references. But it does not work. Shared references are Copy
, and so you can access list
even after it was moved by the loop - but mutable references are not, and so the compiler errs with "borrow of moved value".
The rest of the loops are performing a so-called reborrow, with already explained various combinations of shared/mutable references and using/not using reference patterns. A reborrow is just &*
or &mut *
, and that is dereferencing then immediately taking a reference. Doesn't sound interesting, and indeed it has no effect for shared references, but for mutable references it has important semantics: you cannot copy a mutable reference, but you can reborrow it. &mut *v
gives you an access to a potentially-shorter lifetime, without affecting the original reference. Once the reborrowed reference is no longer needed, you can discard it and use the original reference again.
Since we only reborrow, list
is not moved. Usually the compiler is performing a reborrow automatically - see Do mutable references have move semantics? - but with for
loops, it does not. This is why loop3
failed without the reborrowing, but loop5
succeeeds
As an aside, do not take &Vec<i32>
as parameter, use &[i32]
instead: Why is it discouraged to accept a reference to a String (&String), Vec (&Vec), or Box (&Box) as a function argument?