为什么一个借用的范围不是迭代器,但是范围本身是?

64

范围(range)被使用的一个例子是:

let coll = 1..10;
for i in coll {
    println!("i is {}", &i);
}
println!("coll length is {}", coll.len());

这样会失败

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<i32>`, 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`

通常解决这个问题的方法是借用coll,但在这里不起作用:

Translated text:

通常解决这个问题的方法是借用coll,但在这里不起作用:

error[E0277]: `&std::ops::Range<{integer}>` is not an iterator
 --> src/main.rs:3:14
  |
3 |     for i in &coll {
  |              -^^^^
  |              |
  |              `&std::ops::Range<{integer}>` is not an iterator
  |              help: consider removing the leading `&`-reference
  |
  = help: the trait `std::iter::Iterator` is not implemented for `&std::ops::Range<{integer}>`
  = note: required by `std::iter::IntoIterator::into_iter`
为什么借用范围不是迭代器,但范围是?它是否解释得不同?

1
你正在使用范围,因此它不能用作引用。这与向量的情况不同,例如在其中您实际上会获得内部引用。 - Netwave
3个回答

77

为了理解这里正在发生什么,了解Rust中for循环的工作原理是有帮助的。

基本上,for循环是使用迭代器的简写,因此:

for item in some_value {
    // ...
}

基本上是...的简写

let mut iterator = some_value.into_iter();
while let Some(item) = iterator.next() {
    // ... body of for loop here
}

因此,无论我们使用for循环遍历什么,Rust都会调用IntoIterator特质中的into_iter方法。 IntoIterator特质大致如下:

trait IntoIterator {
    // ...
    type IntoIter;
    fn into_iter(self) -> Self::IntoIter;
}

into_iter方法接受self的值并返回迭代器类型Self::IntoIter。由于Rust将任何按值传递的参数移动,因此在调用.into_iter()后,该对象将不再可用(或在for循环后)。这就是为什么您无法在第一个代码片段中使用coll的原因。

到目前为止一切都好,但为什么我们仍然可以在循环引用的情况下使用集合,如下所示?

for i in &collection {
    // ...
}
// can still use collection here ...

由于很多集合类型C实现了IntoIterator特质,不仅适用于集合本身,还适用于对集合的共享引用&C,该实现会生成共享项。(有时也适用于可变引用&mut C,这会生成对项的可变引用)。
现在回到使用Range的示例,我们可以检查它如何实现IntoIterator
查看 Range 的参考文档,奇怪的是,Range似乎没有直接实现IntoIterator...但是如果我们检查doc.rust-lang.org上的通用实现部分,我们可以看到每个迭代器都实现了IntoIterator特质(通过简单地返回自身)。
impl<I> IntoIterator for I
where
    I: Iterator

这有什么帮助呢?好的,检查更高层次(在特质实现下面),我们可以看到Range确实实现了Iterator
impl<A> Iterator for Range<A>
where
    A: Step, 

因此,Range通过Iterator间接实现了IntoIterator。但是,&Range<A>既没有Iterator的实现(这是不可能的),也没有&Range<A>IntoIterator的实现。因此,我们可以通过传递按值传递的Range来使用for循环,但不能通过引用传递。

为什么&Range不能实现Iterator?迭代器需要跟踪“它所在的位置”,这需要某种形式的突变,但我们不能突变&Range,因为我们只有一个共享引用。所以这行不通。(请注意,&mut Range可以并且确实实现了Iterator - 更多内容稍后介绍)。

技术上讲,为&Range实现IntoIterator是可能的,因为它可以生成一个新的迭代器。但是,这很可能会与Range的通用迭代器实现冲突,这将导致更加混乱。此外,Range最多包含两个整数,复制这些内容非常便宜,因此没有必要为&Range实现IntoIterator

如果您仍然想使用集合,则可以克隆它。

for i in coll.clone() { /* ... */ }
// `coll` still available as the for loop used the clone

这带来了另一个问题:如果我们可以克隆范围并且(如上所述)复制它很便宜,为什么Range不实现Copy trait呢?然后.into_iter()调用将复制范围coll(而不是移动它),并且在循环之后仍然可以使用它。根据此PR,实际上存在Copy trait实现,但被删除,因为以下内容被认为是footgun(感谢Michael Anderson指出这一点):
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();

还要注意,&mut Range实现了迭代器,因此可以这样做

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();

最后,为了完整起见,看一下当我们循环遍历 Range 时实际调用了哪些方法可能是有益的:

for item in 1..10 { /* ... */ }

被翻译为

let mut iter = 1..10.into_iter();
//                   ˆˆˆˆˆˆˆˆˆ--- which into_iter() is this?
while let Some(item) = iter.next() { /* ... */ }

我们可以使用限定方法语法来明确表达这个意思:
let mut iter = std::iter::Iterator::into_iter(1..10);
// it's `Iterator`s  method!  ------^^^^^^^^^
while let Some(item) = iter.next() { /* ... */ }

2
这是一个非常完整的好答案。我一直在考虑它的实现方式,以及与共享引用的区别。最后一行清楚地解释了为什么有些集合可以做到,而Range则不能。 - deitch
1
我很好奇为什么Range没有这样做呢?能否像这样做for i in &coll {...}不是很有用吗? - deitch
我正在处理这个问题,请再给我5分钟时间 ;) - Paul
2
我猜这里唯一缺失的部分就是Range没有实现Copy。如果它实现了,那么在into_iter调用之后,它仍然可以使用。不实现Copy的原因在这里给出:https://github.com/rust-lang/rust/pull/27186 - Michael Anderson
1
我希望我能双倍点赞这个回答。它非常深入和完整。其中有些部分我并不完全理解,但那是因为我需要深入了解特质实际上是如何实现的。我将继续深入挖掘。确实感谢@Paul。 - deitch
显示剩余3条评论

14

范围是能够修改自身来生成元素的迭代器。因此,要循环遍历一个范围,需要修改它(或它的副本,如下所示)。

另一方面,向量本身不是迭代器。在循环遍历向量时,需要调用.into_iter()来创建一个迭代器;向量本身不需要被消耗。

解决方法是使用clone创建一个新的迭代器进行循环遍历:

for i in coll.clone() { 
    println!("i is {}", i);
}

(顺便提一下,println!宏系列会自动获取引用。)


比起我使用 .collect() 方法将它转换为一个 Vec 更加简洁。谢谢! - deitch

3

假设您有一个向量:

let v = vec![1, 2, 3];
Veciter 方法返回实现 Iterator trait 的某个东西。对于向量,还有实现 trait Borrow(和 BorrowMut),但不会返回一个 &Vec。相反,你会得到一个切片 &[T]。可以使用这个切片来迭代向量的元素。

然而,范围(例如 1..10)已经实现了 IntoIterator,不需要转换为切片或其他视图。因此,可以通过调用 into_iter() 来消耗范围本身(该方法会被隐式地调用)。现在,就好像你将范围移动到某个函数中一样,无法再使用变量 coll。借用语法没有帮助,因为这只是 Vec 的一些特殊功能。

在这种情况下,可以使用 collect 方法从范围构建一个 Vec,在迭代时克隆范围,或者在迭代之前获取长度(因为获取长度不会消耗范围本身)。

一些参考文献:


这正是我所做的,尽管我觉得惊讶的是我不能直接获得一个直接的引用。 - deitch
你可以获取到一个 std::ops::Range 的引用,但是它并没有什么用处,因为你需要可变的访问权限来迭代它(也就是消耗它)。这与 Vec 不同,在 Vec 中,“获取引用”有一些更多的魔法,以便你获得一些类型而不是 &Vec,然后允许迭代原始向量拥有的元素。 - Niklas Mohrin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接