Avoid taking ownership?

Rust noob here. Is there a way to not move ownership from chopped.count()? :thinking:

        let chopped = decrypted.split("\r\n");
        let size = chopped.count();  // ownership moved

        ...

        for line in chopped { // ERROR: value used here after move
...
1 Like

The Iterator::count method consumes the iterator, you can't change that. What you can do is call Iterator::collect to create a Vec from the iterator, then call Vec::len to get the count, and then iterate the Vec with your for loop.

1 Like

Alternatively just iterate twice.

let size = decrypted.split("\r\n").count();
2 Likes

Depending on the length of the input, this might be needlessly expensive (two linear scans of the input instead of one).

If speed is important or the input is large, go with a stupidly simple counter (increment it in the for loop).
This also avoids the many allocations necessitated by the collect-to-vec solution.

1 Like

My assumption is that the OP needs the count before entering the for loop. If that's not the case, then what you say above is obviously best.

If you are worried about the expense of iterating twice, you can use inspect with a closure that increments a counter variable in the containing scope when you consume the iterator later. I would provide an example normally, but Iā€™m on mobile.

Edit: ExactSizeIterator might be better.

1 Like
chopped.clone().count()

Many iterators support cloning, and they get cloned themselves (their iteration state), without cloning the underlying data.

There's also iter.by_ref() that technically prevents giving ownership, but the state of iteration will be shared, and count() iterates until the end, so it will "use up" the iterator, and the next for loop will always see 0 items.

3 Likes

Your assumption is correct! :slight_smile:

Yeah, that's what the compiler suggest me to do. :thinking: I am trying to avoid clone() since I assumed it would consume extra memory and feel a bit unnecessary. Unless this is standard in Rust?

The clone is fine and cheap in this case because the underlying iterator (i.e. The struct that implements the Iterator trait returned by split(). This one to be precise) is only (mostly?) holding a reference to the str decrypted.

The work is done while iterating (Someone please correct me if I'm wrong here!) to find the next split each time you call next.

You would do all that work once to find the count, and have to do all that work again to iterate through each split and do your thing.

To avoid doing that work twice, you have the option of saving the splits to a Vec. Which involves the many allocations (Vec will reallocate as it grows larger, this can be mitigated by sizing the Vec up front if you happen to know the length - but since you don't, you can't).

Or you could also avoid doing all the work twice by counting as you do the thing, but then the count isn't available while doing the thing, since you'll only have the final count during the last iteration.

Whether to do the work twice or to allocate a Vec is your choice. I expect the Vec will use more memory, but I can't be sure which will be faster.

Cloning the Iterator itself though should have more or less negligible impact.

5 Likes

That's correct; iterators are lazy (or poorly implemented).

Edit: What I meant is that iterators which are not lazy are generally considered to be poorly implemented (though there are exceptions where being lazy isn't possible or practical).

1 Like

Why are they poorly implemented?

An iterator might do a bunch of work upfront not-so-lazily, which is sometimes unavoidable depending on what one's trying to do, like:

fn sorted_unique<T: Ord>(iter: impl IntoIterator<Item = T>) -> impl Iterator<Item = T> {
    iter.into_iter()
        .collect::<std::collections::BTreeSet<T>>()
        .into_iter()
}

But ideally, one should expect an iterator to do a little as possible until next is called.

2 Likes

Why are iterators that do all their work upfront generally considered poor implementations? Expectations (around behavior / allocation avoidance / latency, etc). There are exceptions where it's unavoidable, but split isn't one of them.

I misread this as "...lazy (a.k.a. poorly implemented) and was also a bit confused.

But I see that you meant they can be lazy or they can be poorly implemented because iterators are supposed to be lazy :slight_smile:

2 Likes

I guess it was poorly implemented phrased. I added a clarification.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.