为什么std::iter::Peekable::peek会可变地借用self参数?

13

我很难理解为什么peek()方法会借用可变的self参数。

文档中说:

“返回对下一个值的引用,而不推进迭代器。”

既然它不会推进迭代器,那么借用参数作为可变的背后有什么意义呢?

我查看了peek()的实现,并注意到它调用了next()方法。

#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn peek(&mut self) -> Option<&I::Item> {
    let iter = &mut self.iter;
    self.peeked.get_or_insert_with(|| iter.next()).as_ref()
}

是因为使用了 next() 方法,peek() 方法才被设计为可变借用的,还是有其他语义需要进行可变借用?

换句话说,当调用 peek() 方法时,会发生什么变化?

2个回答

10

就像你已经做的那样,让我们来看一下它的源代码,这会揭示一些内部工作原理:

pub struct Peekable<I: Iterator> {
    iter: I,
    /// Remember a peeked value, even if it was None.
    peeked: Option<Option<I::Item>>,
}

连同其 next() 实现一起:

impl<I: Iterator> Iterator for Peekable<I> {
    // ...

    fn next(&mut self) -> Option<I::Item> {
        match self.peeked.take() {
            Some(v) => v,
            None => self.iter.next(),
        }
    }

    // ...
}

以及它的peek()实现:

impl<I: Iterator> Peekable<I> {
    // ...

    pub fn peek(&mut self) -> Option<&I::Item> {
        let iter = &mut self.iter;
        self.peeked.get_or_insert_with(|| iter.next()).as_ref()
    }

    // ...
}

Peek 包装 一个现有的迭代器。现有的迭代器是不可预览的。

所以,peek 的作用是:

  • peek() 上:
    • 从包装的迭代器中获取 next() 项并将其存储在 self.peeked 中(如果 self.peeked 尚未包含下一项)
    • 返回对预览项的引用
  • next() 上:
    • 查看我们当前是否有 self.peeked
    • 如果有,则返回该项
    • 如果没有,则从底层迭代器中获取 next() 项。

因此,正如您已经意识到的那样,peek() 操作需要 &mut self,因为它可能需要通过调用底层迭代器上的 next() 来生成下一个预览项。

因此,如果您从更抽象的角度来看待它,则存在以下原因:下一个项可能尚不存在。因此,“窥视”可能涉及实际生成下一个项,这绝对是对基础迭代器的突变操作。

并非所有的迭代器都在数组/切片上,其中项已经存在;迭代器可能是任何生成多个项的东西,包括仅在要求该项时才创建该项的惰性发生器。

他们是否可以以不同的方式实现它?

是的,他们完全有可能以不同的方式实现。他们本来可以在new()期间对基础迭代器进行next()操作。然后,当某人在Peekable上调用next()时,它可以返回当前查看的值并立即查询下一个值。然后,窥视将成为一个&self方法。

为什么他们那样做还不清楚,但最肯定是为了使迭代器尽可能地懒惰。在大多数情况下,惰性迭代器是一件好事。


话虽如此,这里有一个概念验证,展示了如何实现一种预取可查看迭代器,而不需要对peek()使用&mut

pub struct PrefetchingPeekingIterator<I: Iterator> {
    iter: I,
    next_item: Option<I::Item>,
}

impl<I: Iterator> PrefetchingPeekingIterator<I> {
    fn new(mut iter: I) -> Self {
        let next_item = iter.next();
        Self { iter, next_item }
    }

    fn peek(&self) -> Option<&I::Item> {
        self.next_item.as_ref()
    }
}

impl<I: Iterator> Iterator for PrefetchingPeekingIterator<I> {
    type Item = I::Item;

    fn next(&mut self) -> Option<Self::Item> {
        std::mem::replace(&mut self.next_item, self.iter.next())
    }
}

fn main() {
    let mut range = PrefetchingPeekingIterator::new(1..10);
    dbg!(range.next().unwrap());
    dbg!(range.peek().unwrap());
    dbg!(range.next().unwrap());
    dbg!(range.peek().unwrap());
    dbg!(range.next().unwrap());
    dbg!(range.peek().unwrap());
}

[src/main.rs:27] range.next().unwrap() = 1
[src/main.rs:28] range.peek().unwrap() = 2
[src/main.rs:29] range.next().unwrap() = 2
[src/main.rs:30] range.peek().unwrap() = 3
[src/main.rs:31] range.next().unwrap() = 3
[src/main.rs:32] range.peek().unwrap() = 4

啊,现在我明白了,在next()方法中的take()调用是移除了预览的值,这样当我们再次调用next()时就会得到一个新的值。 - anilbey
1
@anilbey,我已经添加了源代码,提供了一个可预览迭代器的另一种实现方式,它不需要使用 &mut。也许这可以满足您的使用需求。 - Finomnis
调用 peak() 首先会调用 next(),这将删除峰值并使用 get_or_insert_with 重新插入相同的值,对吗? - anilbey
1
get_or_insert_with() 只有在没有值的情况下才会调用 next()。请注意,它前面有 ||,使其成为一个闭包。 - Finomnis
为了保持peek的非变异性,他们也可以使用内部变异,但是如果他们使用RefCell,它就不会是Sync,如果他们使用Mutex,即使不需要,你也会付出额外的性能代价。 - rodrigo
很好,详细,谢谢! - James Risner

4

是的,正如您注意到的那样,Peekable::peek 可能需要调用 self.iter.next() 来获取元素,如果它的 self.peeked 中没有存储任何内容。 然后,它还必须将该值存储在某个位置,以不使 Peekable 迭代器前进。 底层的 Iterator 很可能会由此而前进。

既要推进底层的 Iterator,又要将该值存储在 self.peeked 中,都需要对 self 进行可变访问。


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