何时使用 iter() 与 into_iter()?

解读

在国内 Rust 岗位面试中,这道题几乎是“必考题”。面试官想确认两点:

  1. 你是否真正理解 所有权转移借用 的差异;
  2. 你是否能在 性能、内存、接口设计 三个维度做出权衡。
    答不上来,会被直接打上“只写过玩具代码”的标签;答得太浅,会被追问“那如果集合元素是 String,你选哪个?”因此,必须给出 场景化、可落地的判断逻辑,而不是背定义。

知识点

  1. iter() —— 返回 &T 的迭代器,不消耗原集合,底层是 借用(&self)
  2. into_iter() —— 返回 T 的迭代器,会转移所有权(self),底层是 Move
  3. 语法糖:for 循环 默认调用 into_iter(),因此 for x in vec 会把 vec move 掉;若不想失去所有权,必须显式 for x in &vec(等价于 iter())。
  4. 性能差异:iter() 只是指针步进,零成本;into_iter() 对 Vec 会 回收底层堆内存,对 Copy 类型(如 u32)编译器会优化为 按值拷贝,无额外开销;对 非 Copy 类型(如 String)会 逐元素 Move,可能触发 二次分配
  5. 接口设计:若你的函数 只需读取元素,用 impl IntoIterator<Item=&T> 接受 iter();若函数 需要获得元素所有权(如异步发送、转存到另一个线程),用 impl IntoIterator<Item=T>建议调用者自行决定是否 into_iter(),避免 隐式 Move 导致编译失败。

答案

先给出口诀,再给场景,最后给代码骨架,面试官最爱这种“总分总”结构。
口诀:“只读借 iter,要走所有 into;Copy 类型随便选,String 元素慎 move。”

场景一:只读遍历,后续还要用原集合

let v = vec!["Beijing", "Shanghai"];
for city in v.iter() {          // &str
    println!("{}", city);
}
// v 仍可用

iter(),因为 不丢失所有权,且 元素是 &str,无需 Move。

场景二:消费集合,把元素传到另一个线程

let v = vec!["hello".to_string(), "rust".to_string()];
std::thread::spawn(move || {
    for s in v.into_iter() {    // String
        send_to_remote(s);      // 需要所有权
    }
});

into_iter(),因为 String 必须 Move,且 原集合后续不再使用

场景三:封装库 API,既要灵活又要零成本

pub fn process<I, T>(iter: I)
where
    I: IntoIterator<Item=T>,
    T: AsRef<str>,
{
    for s in iter {
        println!("{}", s.as_ref());
    }
}
// 调用者
let v = vec!["a", "b"];
process(v.iter().copied());   // 借用
process(v.into_iter());       // 所有权

IntoIterator 泛型,把选择权交给用户,库本身 零拷贝、零额外内存

一句话总结:“后续还要用集合, iter();只要里面的值, into_iter();API 设计用泛型 IntoIterator,既安全又灵活。”

拓展思考

  1. 自定义集合如何同时支持 iter() 与 into_iter()?
    实现 impl<'a> IntoIterator for &'a MyVec 返回 &T 迭代器,再实现 impl IntoIterator for MyVec 返回 T 迭代器。面试时能把 双 trait 实现 写出来,直接加分。

  2. async 场景下为什么更倾向 into_iter()?
    异步任务常常要把数据 move 到 Future,若用 iter() 会留下 生命周期约束,导致 Future 无法成为 'static,必须用 into_iter()collect::<Vec<_>>().into_iter() 把数据 完全转移 到堆上,解除生命周期绑定。

  3. 大集合性能优化:into_iter().collect() 会复用原 Vec 的容量吗?
    不会。into_iter() 会把 Vec 解构为 RawVec,collect() 会 重新分配。若需 原地复用容量,应使用 Vec::into_raw_parts(nightly)或 drain_filter 技巧,这是 国内大厂性能敏感模块 的常用手段,答出来可展示 底层功力