patternrustMajor
Why is a borrowed range not an iterator, but the range is?
Viewed 0 times
borrowedwhyiteratorthenotbutrange
Problem
An example of how a range gets consumed is:
This will fail with
The usual way to fix this is to borrow the
Why is that? Why is a borrowed range not an iterator, but the range is? Is it interpreting it differently?
let coll = 1..10;
for i in coll {
println!("i is {}", &i);
}
println!("coll length is {}", coll.len());
This will fail with
error[E0382]: borrow of moved value: coll
--> src/main.rs:6:35
|
2 | let coll = 1..10;
| ---- move occurs because coll has type std::ops::Range, which does not implement the Copy trait
3 | for i in coll {
| ----
| |
| coll moved due to this implicit call to .into_iter()
| help: consider borrowing to avoid moving into the for loop: &coll
...
6 | println!("coll length is {}", coll.len());
| ^^^^ value borrowed here after move
|
note: this function consumes the receiver self by taking ownership of it, which moves coll
The usual way to fix this is to borrow the
coll, but that doesn't work here:error[E0277]: &std::ops::Range is not an iterator
--> src/main.rs:3:14
|
3 | for i in &coll {
| -^^^^
| |
| &std::ops::Range is not an iterator
| help: consider removing the leading &-reference
|
= help: the trait std::iter::Iterator is not implemented for &std::ops::Range
= note: required by std::iter::IntoIterator::into_iter
Why is that? Why is a borrowed range not an iterator, but the range is? Is it interpreting it differently?
Solution
To understand what is happening here it is helpful to understand how for loops work in Rust.
Basically a for loop is a short hand for using an iterator, so:
is basically a short-hand for
So we can see that whatever we loop over with the for loop, Rust calls the
So
So far so good, but why can we still use a collection if we loop over a reference of it as in the following?
The reason is that for a lot of collections
Now coming back to the example with the
Looking at the reference docs for Range,
How does this help? Well, checking further up (under trait implementations) we see that
And thus
Why can
It would technically be possible to implement
If you still want to use collection, you can clone it
This brings up another question: If we can clone the range and it is (as claimed above) cheap to copy it, why doesn't Range implement the
Also note that
Finally, for completeness, it might be instructive to see which methods are actually called when we loop over a Range:
is translated to
we can make this explicit using qualified method syntax:
Basically a for loop is a short hand for using an iterator, so:
for item in some_value {
// ...
}
is basically a short-hand for
let mut iterator = some_value.into_iter();
while let Some(item) = iterator.next() {
// ... body of for loop here
}
So we can see that whatever we loop over with the for loop, Rust calls the
into_iter method from the IntoIterator trait on. The IntoIterator trait looks (approximately) like this:trait IntoIterator {
// ...
type IntoIter;
fn into_iter(self) -> Self::IntoIter;
}So
into_iter takes self by value and returns Self::IntoIter which is the type of the iterator. As Rust moves any arguments which are taken by value, the thing .into_iter() was called on is no longer available after the call (or after the for loop). That's why you can't use coll in your first code snippet.So far so good, but why can we still use a collection if we loop over a reference of it as in the following?
for i in &collection {
// ...
}
// can still use collection here ...
The reason is that for a lot of collections
C, the IntoIterator trait is implemented not just for the collection, but also for a shared reference to the collection &C and this implementation produces shared items. (Sometimes it is also implemented for mutable references &mut C which produces mutable references to items).Now coming back to the example with the
Range we can check how it implements IntoIterator.Looking at the reference docs for Range,
Range strangely does not seem to implement IntoIterator directly... but if we check the Blanket Implementations section on doc.rust-lang.org, we can see that every iterator implements the IntoIterator trait (trivially, by just returning itself):impl IntoIterator for I
where
I: IteratorHow does this help? Well, checking further up (under trait implementations) we see that
Range does implement Iterator:impl Iterator for Range
where
A: Step,
And thus
Range does implement IntoIterator via the indirection of Iterator. However, there is no implementation of either Iterator for &Range (this would be impossible) or of IntoIterator for &Range. Therefore, we can use a for loop by passing Range by value, but not by reference.Why can
&Range not implement Iterator? An iterator needs to keep track of "where it is", which requires some kind of mutation, but we cannot mutate a &Range because we only have a shared reference. So this cannot work. (Note that &mut Range can and does implement Iterator - more on this later).It would technically be possible to implement
IntoIterator for &Range as that could produce a new iterator. But the likelihood that this would clash with the blanket iterator implementation of Range would be very high and things would be even more confusing. Besides, a Range is at most two integers and copying this is very cheap, so there is really no big value in implementing IntoIterator for &Range.If you still want to use collection, you can clone it
for i in coll.clone() { / ... / }
// coll still available as the for loop used the clone
This brings up another question: If we can clone the range and it is (as claimed above) cheap to copy it, why doesn't Range implement the
Copy trait? Then the .into_iter() call would copy the range coll (instead of moving it) and it could still be used after the loop. According to this PR the Copy trait implementation actually existed but was removed because the following was considered a footgun (hat tip to Michael Anderson for pointing this out):let mut iter = 1..10;
for i in iter {
if i > 2 { break; }
}
// This doesn't work now, but if Range implemented copy,
// it would produce [1,2,3,4,5,6,7,8,9] instead of
// [4,5,6,7,8,9] as might have been expected
let v: Vec = iter.collect();
Also note that
&mut Range does implement iterator, so you can dolet mut iter = 1..10;
for i in &mut iter {
if i > 2 { break; }
}
// [4,5,6,7,8,9] as expected
let v: Vec = iter.collect();
Finally, for completeness, it might be instructive to see which methods are actually called when we loop over a Range:
for item in 1..10 { / ... / }
is translated to
let mut iter = 1..10.into_iter();
// ˆˆˆˆˆˆˆˆˆ--- which into_iter() is this?
while let Some(item) = iter.next() { / ... / }
we can make this explicit using qualified method syntax:
let mut iter = std::iter::Iterator::into_iter(1..10);
// it's Iterators method! ------^^^^^^^^^
while let Some(item) = iter.next() { / ... / }
Code Snippets
trait IntoIterator {
// ...
type IntoIter;
fn into_iter(self) -> Self::IntoIter;
}impl<I> IntoIterator for I
where
I: IteratorContext
Stack Overflow Q#63685464, score: 80
Revisions (0)
No revisions yet.