DEBUG.WIKI
腾讯云;让你的想法跑起来;轻量服务器 · 建站 / Docker / API;查看产品 →
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →

Rust 的 nth(0) 与 next() 结果不同,检查非融合迭代器上的 skip 和 chain

Rust 1.99.1 的非融合迭代器组合 skip、chain 时,nth(0) 可能与 next 不同。按业务终止含义选 next 或底层 fuse。

同一份新建迭代器,调用 next() 得到 None,换成 nth(0) 却得到 Some。如果输入允许先返回 None、以后又返回 Some,并且组合了 skip 与 chain,这可能是适配器内部推进方式不同,不能先把差异归为业务状态被并发修改。

Rust 1.99.1 的具体形态如下。它不含线程,Gap 第一次返回 None,之后继续产生整数:

struct Gap(u32);

impl Iterator for Gap {
    type Item = u32;
    fn next(&mut self) -> Option<u32> {
        self.0 += 1;
        if self.0 == 1 { None } else { Some(self.0 - 1) }
    }
}

fn main() {
    let first = Gap(0).skip(1).chain(std::iter::empty()).next();
    let other = Gap(0).skip(1).chain(std::iter::empty()).nth(0);
    println!("{:?} {:?}", first, other);
}

受影响组合会得到 None Some(2)。这里特意创建两个独立实例;若在同一个实例上先调用 next 再调用 nth,前一次消费自然会影响结果,不能据此判断相同问题。

None 后继续产出,并不违反 Iterator 的一般约定

普通 Iterator 可以在返回 None 后继续返回 Some。FusedIterator 才提供终止后持续 None 的保证。文件流、集合迭代器常常具备终止语义,但自定义迭代器不一定如此。

1.99.1 的 Chain::nth 会先让前半段 advance_by(n),再取下一项。即使 n 为零,Skip 的推进实现仍可能消耗它内部待跳过的元素;第一次碰到 None 时,待跳过数量的保留方式又与 Skip::next 不同。这使“取第零项”经历了与直接 next 不同的内部状态变化。

不能简单把适配器里的计数清零当作完整修复:底层迭代器可能已经被推进,清掉计数也不会退回原来的位置。本文不提供手改标准库的补丁,也不由问题关闭推断某个维护版本已经修好。

按业务的终止含义选择写法

如果调用点只需要取下一项,直接用 next(),保留本来就能表达的消费动作。不要为统一调用形式而把所有 next 机械换成 nth(0),尤其是还组合了其他适配器时。

如果第一次 None 在业务上就应表示永久结束,可以在底层迭代器进入 skip 和 chain 之前先融合:

let value = Gap(0)
    .fuse()
    .skip(1)
    .chain(std::iter::empty())
    .nth(0);

这时 Gap 第一次返回 None,后面的整数将不会再被观察到。它有意改变了终止后的语义,只有业务本来就要终止时才适用。把 fuse 仅加在整个 chain 的最外层,并不能保证首次调用进入内部适配器时避开本题。

若 None 代表“目前还没有”,以后仍应继续处理,就不要靠 fuse 把后续数据永久屏蔽。更合适的是把“暂时无数据”表示成迭代项内的明确状态,或者使用适合等待与唤醒的接口;Iterator 的 None 很容易被适配器当作遍历结束。

复查时分别创建新实例比较 next 与 nth(0),覆盖普通有限迭代器、先 None 后 Some 的输入,以及业务所选的终止策略。不能只比较最终收集到的向量:收集本身在遇到 None 时停止,可能掩盖你原本想保留的后续状态。