1

I want to iterate over a collection type in a struct and remove some values, but Rust prevents me from destroying the collection:

fn some_method(&mut self) {
    self.collection = self
        .collection
        .into_iter()
        .filter(/* ... */
        .collect();
}

I could clone all of the values to build another collection, but that's not efficient. What's the idiomatic way of removing a value from the collection in place in Rust?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Jiaming Lu
  • 875
  • 6
  • 20
  • 3
    What should be in `self.collection` if the `filter` callback were to panic part way through? Please provide a more complete example snippet, for instance, is `collection` a `Vec`? If it is, then `.drain(..)` instead of `into_iter()` would work here because it doesn't take ownership. There is also be `.retain(...)`. – loganfsmyth Sep 30 '19 at 04:05
  • @loganfsmyth In my case the collection is a `BTreeMap`, which doesn't have a `drain` method. Do I really need to care about panics? In rust, panics are true panics, not control flows that should be handled right? – Jiaming Lu Sep 30 '19 at 06:46
  • @loganfsmyth And what I want is "remove while iterating", something like `iterator.remove()` in Java. – Jiaming Lu Sep 30 '19 at 06:48
  • Probably you are looking for [this](https://stackoverflow.com/questions/32913368/removing-items-from-a-btreemap-or-btreeset-found-through-iteration) – Akiner Alkan Sep 30 '19 at 07:03
  • 2
    Possible duplicate of [Removing items from a BTreeMap or BTreeSet found through iteration](https://stackoverflow.com/questions/32913368/removing-items-from-a-btreemap-or-btreeset-found-through-iteration) – Akiner Alkan Sep 30 '19 at 07:03
  • 1
    @JiamingLu Rust's iterators can iterate over things that are immutable. Can Java's iterators do that, and if so what does `remove` do? – trent Sep 30 '19 at 13:48
  • @trentcl java iterator.remove removes the current item. And I f you call remove on the container directly while iterating results in an exception. – Jiaming Lu Oct 01 '19 at 01:07

2 Answers2

1

A full example (also with into_iter):

#[derive(Debug)]
struct Scores {
    collection: Vec<i32>,
}

impl Scores {
    fn new() -> Scores {
        return Scores {
            collection: Vec::new(),
        };
    }

    fn filter_in_above_50(&mut self) {
        self.collection = self
            .collection
            .drain(..)
            .filter(|score| score > &50)
            .collect();
    }

    fn filter_in_above_50_using_into_iter(&mut self) {
        let coll: &mut Vec<i32> = self.collection.as_mut();
        let coll: Vec<i32> = coll
            .into_iter()
            .filter(|score| score > &&mut 50i32)
            .map(|&mut x| x)
            .collect();
        self.collection = coll;
    }
}

And the tests:

#[test]
fn score_test() {
    let mut s = Scores::new();
    s.collection.push(199);
    s.collection.push(11);
    s.filter_in_above_50();
    assert_eq!(s.collection, vec![199]);
}

#[test]
fn score_test_using_into_iter() {
    let mut s = Scores::new();
    s.collection.push(199);
    s.collection.push(11);
    s.filter_in_above_50_using_into_iter();
    assert_eq!(s.collection, vec![199]);
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Kajal Sinha
  • 1,565
  • 11
  • 20
  • Is it a good idea to print something in test instead of asserting? – Cerberus Sep 30 '19 at 06:16
  • Not really, fixed that! Thanks for highlighting :) – Kajal Sinha Sep 30 '19 at 06:22
  • But `drain` only works for `Vec`, no such method for `BTreeMap` or `HashMap` – Jiaming Lu Sep 30 '19 at 06:40
  • 1
    @JiamingLu, What is your case exactly?. This answer seems convenient for the Vec. If you edit the question to be more specific, you can get better and more specific answers. – Akiner Alkan Sep 30 '19 at 06:48
  • @Websterix Initially I'm asking about some general patterns, no matter what type of the collection is. For example in Java, I have iterator.remove(). But now it seems in Rust there is only case by case solutions. – Jiaming Lu Sep 30 '19 at 06:50
  • @JiamingLu does the second example test solves your problem? Though it works as I tested but I am interested to learn and know more about the performance impact of second approach. – Kajal Sinha Sep 30 '19 at 07:13
  • 1
    `filter_in_above_50_using_into_iter` doesn't use `into_iter` on `Vec`; it just uses `into_iter` on `&mut Vec`, which is weird in a non-generic context (you could use `.iter_mut()` to be more expressive and concise). It also requires `T` to be `Copy`. In general it's just kind of weird and unidiomatic. I don't recommend writing code like that. (The `drain(..)` version looks fine though.) – trent Sep 30 '19 at 13:46
1

Since collection is a BTreeMap and does not have .drain() or .retain, and you're fine with emptying the tree during processing, the thing to do would be to move the tree out of self.collection, manipulate it how you want, and then put it back.

As you've seen, Rust doesn't allow that with simple assignment, because if there were a panic during the time when your snippet runs, self.collection would be left in an inconsistent state. Instead, you need to do that explicitly using std::mem::replace, which allows you to take ownership of the content in a mutable reference by providing a replacement for it, so the reference continues to point at valid data.

fn some_method(&mut self) {
  self.collection = std::mem::replace(&mut self.collection, BTreeMap::new())
    .into_iter()
    .filter(|_| true)
    .collect();
}
loganfsmyth
  • 156,129
  • 30
  • 331
  • 251