Why is a borrowed range not an iterator, but the range is?
Let's say you have a vector:
let v = vec![1, 2, 3];
The method iter
on Vec
returns something that implements the Iterator
trait. With a vector, there is also an implementation of the trait Borrow
(and BorrowMut
), that does not return a &Vec
though. Instead, you get a slice &[T]
. This slice can then be used to iterate over the elements of the vector.
However, the range (e.g. 1..10
) implements IntoIterator
already and does not need to be transformed into a slice or some other view into it. Therefore, you can consume the range itself by calling into_iter()
(which you do implicitly). Now, it is as if you moved the range into some function and you cannot use your variable coll
anymore. The borrowing syntax won't help, since this is only some special functionality of Vec
.
In this case, you could construct a Vec
from your range (with the collect
method), clone the range when iterating over it or get the length before iterating (since getting the length doesn't consume the range itself).
Some references:
- https://doc.rust-lang.org/std/vec/struct.Vec.html
- https://doc.rust-lang.org/std/primitive.slice.html
- https://doc.rust-lang.org/std/ops/struct.Range.html
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:
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<I> IntoIterator for I
where
I: Iterator
How does this help? Well, checking further up (under trait implementations) we see that Range
does implement Iterator
:
impl<A> Iterator for Range<A>
where
A: Step,
And thus Range
does implement IntoIterator
via the indirection of Iterator
. However, there is no implementation of either Iterator
for &Range<A>
(this would be impossible) or of IntoIterator
for &Range<A>
. 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 do
let 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 `Iterator`s method! ------^^^^^^^^^
while let Some(item) = iter.next() { /* ... */ }
Ranges are iterators that modify themselves to generate elements. Therefore, to loop over a range, it is necessary to modify it (or a copy of it, as shown below).
Vectors, on the other hand, are not iterators themselves. .into_iter()
is called to create an iterator when a vector is looped over; the vector itself doesn't need to be consumed.
The solution here is to use clone
to create a new iterator that can be looped over:
for i in coll.clone() {
println!("i is {}", i);
}
(Incidentally, the println!
family of macros take references automatically.)