Home > Mobile >  What are the differences between my for loop variations in Rust?
What are the differences between my for loop variations in Rust?

Time:07-01

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 and loop2 (I believe 3 and 4, 5 and 6 are analogous)
  • The inline question in loop1 and loop2
  • 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 &i32s. 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?

  • Related