Idiomatic way to take ownership of all items in a Vec<String>?

I have a function that reads a 2-column CSV file (using the csv crate) and loads each row into a BTreeMap. To do this, I need to move the Strings out of the Vec<String> that the reader gives me, and into the map. After an initial solution using into_iter, I settled on just pulling them out with mem::replace:

for row in reader.records() {
    let mut row = row.unwrap();
    let key = mem::replace(&mut row[0], String::new());
    let val = mem::replace(&mut row[1], String::new());
    map.insert(key, val);
}

This works fine, but is there a more idiomatic way to take ownership of all the items in a Vec?

The most basic way to take ownership of a vector element is to remove it from the Vec. For that reason, this will be fine:

    let key = row.remove(0);
    let val = row.remove(0);

This time of course adjusting the index of the second remove after how they shifted due to the first. It's also good to be aware of the vector's swap_remove method.

3 Likes

IntoIterator + collect() is the proper way to do it but you'll need a custom iterator after into_iter() to group your elements 2 by 2.
Have a look at the itertools crate. I think something like Tuples might be what you want.

2 Likes

Here's a quick and dirty version of the idea:

use std::collections::HashMap;

fn main() {
    let list: Vec<Vec<String>> = vec![vec!["foo".into(), "bar".into()], vec!["batz".into(), "bla".into()]];
    
    let map = list.into_iter().map(|mut row| { (row.remove(0), row.remove(1)) }).collect::<HashMap<_,_>>();
}

You can directly collect into the HashMap for that.

1 Like

There's also Vec::drain.

5 Likes

Yes, that works in my example, but that makes the rather specific assumption that we have a vector at hand. Not all indexable things implement it.

Well, @dimfeld specifically asked about Vec:

1 Like

My initial solution used into_iter on the Vec to accomplish pretty much the same thing as drain. It just felt a little weird since for this really simple case I ended up just calling next twice instead of actually using the more functional-style Iterator functions.

Using remove seems obvious now that you mentioned it :slight_smile: Will keep that in mind for next time since I don't actually need to keep the Vec intact`

I also overlooked that the map types implement collect; that's so cool! And perfect for this particular case.

Thanks, everyone, for these great answers!

Be careful with remove() though, it will shift all the elements after the one removed by one to the left. Repeated calls to remove(0) is a big code smell. drain() and into_iter() are a lot more efficient here.

4 Likes

Oh, for sure. My case here is somewhat special since the Vec will always have exactly 2 items in it. I was planning to use vec.remove(1); vec.remove(0); to avoid the shift, but into_iter definitely makes more sense for more general cases or larger vectors.

1 Like

For your use case, swap_remove(0) is going to be what you want. It doesn't allocate two pointless strings as does mem::replace and it doesn't shift as with remove(). It should give you the same memcpy as doing remove in reverse, but offers the additional benefit of your code not looking like it is doing something in a weird order. Also, no sense is making something generic and using into_iter if you will only ever have 2 elements.

Small correction: String::new() doesn't allocate.

2 Likes

Sure it does, Rust Playground. Creating a new anything allocates something. Not to mention it still must hit whatever branch logic exists in its Drop implementation.

You probably meant it doesn't allocate any memory from the heap. That is true. I know the cost of this is minuscule, and maybe LLVM does some neat tricks, but it is clearly doing more work calling String::new() than if that wasn't there.

Creating a new anything allocates something.

I don't think that's technically true. Rust has zero-sized types, like unary types, that usually reduce to no-ops when instantiated or stored.

Yeah I'm quibbling here, but I thought you deserved it. :slight_smile:

I was referring to anything with an impl and a new() fn, but yes - I deserved it :slight_smile:

Which is what is usually meant by "allocate" around here. Writing an empty String into a Vec<String> doesn't even have to put anything on the stack, it just has to overwrite some already-allocated bytes in the Vec backing store. I don't see how that is "allocating".

2 Likes