【问题标题】:Why is a borrowed range not an iterator, but the range is?为什么借用的范围不是迭代器,但范围是?
【发布时间】:2020-12-20 10:31:14
【问题描述】:

一个范围如何被消耗的例子是:

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,但这在这里不起作用:

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`

这是为什么呢?为什么借用的范围不是迭代器,但范围是?是不是有不同的解释?

【问题讨论】:

  • 您正在使用该范围,因此它不能用作参考。例如,实际上在其中获取引用的向量与向量不同。

标签: rust reference iterator


【解决方案1】:

要了解这里发生了什么,了解 for 循环在 Rust 中是如何工作的会很有帮助。

基本上,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 循环之后)不再可用。这就是为什么你不能在你的第一个代码 sn-p 中使用coll

到目前为止一切都很好,但是如果我们像下面这样循环它的引用,为什么我们仍然可以使用集合?

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

原因是对于很多集合CIntoIterator trait 不仅针对集合实现,还针对集合&amp;C 的共享引用实现,并且此实现产生共享项。 (有时它也被实现为可变引用&amp;mut C,它产生对项目的可变引用)。

现在回到Range 的示例,我们可以检查它如何实现IntoIterator

看看the reference docs for RangeRange 奇怪地似乎没有直接实现IntoIterator...但是如果我们查看 doc.rust-lang.org 上的Blanket Implementations 部分,我们可以看到每个迭代器都实现了IntoIterator 特征(很简单,只返回自身):

impl<I> IntoIterator for I
where
    I: Iterator

这有什么帮助?好吧,检查further up(在特征实现下)我们看到Range确实实现了Iterator

impl<A> Iterator for Range<A>
where
    A: Step, 

因此Range 确实通过Iterator 的间接实现IntoIterator。但是,对于&amp;Range&lt;A&gt;(这是不可能的)或IntoIterator 对于&amp;Range&lt;A&gt;,没有实现Iterator。因此,我们可以使用 for 循环,通过值传递 Range,而不是通过引用传递。

为什么&amp;Range不能实现Iterator?迭代器需要跟踪“它在哪里”,这需要某种变异,但我们不能变异&amp;Range,因为我们只有一个共享引用。所以这行不通。 (请注意,&amp;mut Range 可以并且确实实现了Iterator - 稍后会详细介绍)。

从技术上讲,为&amp;Range 实现IntoIterator 是可行的,因为这可能会产生一个新的迭代器。但是,这与Range 的全面迭代器实现发生冲突的可能性非常高,事情会更加混乱。此外,Range 最多是两个整数,复制它非常便宜,因此为&amp;Range 实现IntoIterator 确实没有太大价值。

如果你还想使用collection,你可以克隆它

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

这带来了另一个问题:如果我们可以克隆范围并且(如上所述)复制它很便宜,那么为什么 Range 不实现 Copy 特征?然后.into_iter() 调用将复制范围coll(而不是移动它)并且在循环之后仍然可以使用它。 According to this PR Copy trait 实现确实存在,但被删除了,因为以下内容被认为是一种猎枪(向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();

还要注意&amp;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() { /* ... */ }

【讨论】:

  • 当这是一个很好的完整答案。我认为该特征已在其上实现,并且与共享参考不同。最后一行清楚地解释了原因,例如有些收藏可以做到,而Range 做不到。
  • 我很好奇为什么Range 也没有这样做?能够做到for i in &amp;coll {...}不是有用吗?
  • 我要开始了,再给我 5 分钟 ;)
  • 我想这里唯一缺少的部分是Range 没有实现Copy。如果确实如此,那么在其into_iter 调用之后仍然可以使用。这里给出了没有实现 Copy 的原因github.com/rust-lang/rust/pull/27186
  • 我希望我能加倍投票。这是相当深入和完整的答案。我不完全理解其中的一部分,但那是因为我需要深入研究特征的实际实现方式。我要挖掘更多。谢谢你@Paul。
【解决方案2】:

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

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

这里的解决方案是使用clone创建一个可以循环的新迭代器:

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

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

【讨论】:

  • 比我所做的要干净得多,我使用 .collect() 将其转换为 Vec。谢谢!
【解决方案3】:

假设你有一个向量:

let v = vec![1, 2, 3];

Vec 上的方法iter 返回实现Iterator 特征的一些东西。使用向量,还有一个特征Borrow(和BorrowMut)的实现,但它不会返回&amp;Vec。相反,您会得到一个切片&amp;[T]。然后可以使用这个切片来迭代向量的元素。

但是,范围(例如1..10)已经实现了IntoIterator,不需要将其转换为切片或其他视图。因此,您可以通过调用 into_iter()(您隐式执行)来使用范围本身。现在,就好像您将范围移动到某个函数中并且您不能再使用您的变量coll。借用语法无济于事,因为这只是Vec 的一些特殊功能。

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

一些参考资料:

【讨论】:

  • 这正是我所做的正是,尽管我发现我无法获得直接的参考,这让我很惊讶。
  • 好吧,您可以获得对 std::ops::Range 的引用,但是它没有用,因为您需要可变访问来迭代它(也就是使用它)。这与Vec 形成对比,其中“获取引用”具有更多魔力,因此您可以获得除&amp;Vec 之外的其他类型,然后允许迭代原始向量拥有的元素。
猜你喜欢
  • 2014-03-15
  • 2020-04-03
  • 1970-01-01
  • 2020-10-14
  • 1970-01-01
  • 2016-06-29
相关资源
最近更新 更多