References And Iteration in Rust

Tags: rust technical

References and Iteration

There’s a slight possibility that what I’ve written here may not be 100% correct. In such case I’ll come back and update my stance as I learn more about the language.

As a Rust noob, yesterday I came to know about the “pattern” of iteration. It’s like the combination of having two things: rust passing you the references of values and how to use them, and how the values are owned/copied/moved in all of this. There was a good enough discussion that I had on #rust-beginners where the community was really helpful.

So iteration is done using for keyword, and while some data types provide iteration out of the box, I was mostly looking at iterating BTreeMap or HashMap, let’s call them containers.

Iterating a container also depends on whether it contains a value which implements Copy or not. Copy is a trait, which if implemented by a type allows the type to move by copying its value instead of copying the references using clone() . Most of the primitives like i32, f32, etc implement it. Other types like &str, String, Vec<T> do not, and that’s why we cannot move their borrowed copies. A more detailed discussion is available at StackOverflow

To do this iteration, rust provides a pattern:

let hashmap: HashMap<T1, T2> = HashMap::new();

// fill the hash with values

for (k, v) in hashmap.iter() {
    // use k and v
}

both k and v are actually references to the keys and values of the hashmap, and are used as such, so these have the types &T1 and &T2 respectively.

In case I write the following:

let hashmap: HashMap<T1, T2> = HashMap::new();
for (&k, &v) in hashmap.iter() {

}

the k and v inside the for expression become the values, instead of becoming the references. Rust pattern matches and removes the reference operator before passing the values inside the for. The values now have the types T1 and T2 respectively.

This matters in case k or v implement Copy or not. In case they do, then nothing bad happens (like i32). In case they don’t (eg. Vec, HashMap), then v in the first snippet is actually &Vec<T>, or &HashMap<T1, T2> inside the for expression, and Vec<T> in the second one. So usage depends on what we are doing.

For example, here’s an iteration of HashMap<i32, char>:

fn main() {

    let mut copy_types: HashMap<i32, char> = HashMap::new();
    copy_types.insert(12, 'a');
    copy_types.insert(13, 'b');

    iterate_copy_types_with_ref(&copy_types);
    iterate_copy_types_without_ref(&copy_types);
}

fn iterate_copy_types_with_ref(h: &HashMap<i32, char>) {
    // Taking a reference of non-copy types works
    for (&k, &v) in h.iter() {
        println!("{}, {}", k, v);
    }
}

fn iterate_copy_types_without_ref(h: &HashMap<i32, char>) {
    // Not taking a reference of non-copy types works as well
    for (k, v) in h.iter() {
        println!("{}, {}", k, v);
    }
}

Using & inside the iterator doesn’t matter because both i32 and char are Copy and the compiler copies these.

In case the type does not implement Copy like a Vec<T>, then it becomes important how we use the references with iterators.

fn main() {
    let mut ncopy_types: HashMap<i32, Vec<&str>> = HashMap::new();
    // Vec<T> is a non copy type.
    ncopy_types.insert(12, vec!["My", "name", "is"]);
    ncopy_types.insert(13, vec!["your", "name", "is"]);

    iterate_non_copy_types(ncopy_types);
}

fn iterate_non_copy_types(h: HashMap<i32, Vec<&str>>) {
    // v is actually a `&Vec<&str>` here.
    for (&k, v) in h.iter() {
        println!("{}, {:?}", k, v);
        // 13, ["your", "name", "is"]
        // 12, ["My", "name", "is"]
    }

    for (&k, v) in h.iter() {
        // Again, since c implements Copy, there's no need actually to
        // use &c -- we can use `c` and it will work fine.
        for &c in v {
            println!("{}", c);
        }
    }
}