&str 与 String 的所有权差异?

解读

在国内 Rust 岗位面试中,这道题几乎必问,因为它同时考察“所有权系统”“内存布局”和“常用 API 设计哲学”三个维度。面试官往往不会满足于“一个可变一个不可变”这种表面答案,而是希望候选人能用生命周期、内存分配位置、Drop 语义把差异讲透,并能结合实战代码说明何时用 &str、何时用 String,以及混用时会触发哪些编译器报错。

知识点

  1. 所有权三要素:拥有者、作用域、Drop
  2. 内存位置:String 在上持有动态缓冲区;&str 只是胖指针(指针+长度),不拥有内存
  3. Drop 语义:String 实现 Drop,出作用域自动释放堆内存;&str 是 Copy 类型,无析构负担
  4. 生命周期:&str 必须受借用检查器约束,生命周期不能超过指向的数据;String 作为自有类型,生命周期与变量绑定,无需额外标注
  5. 转换开销:String → &str 零成本解引用强制转换(Deref coercion);&str → String 必须堆分配并拷贝字节,O(n) 开销
  6. API 设计惯例:函数参数优先用 &str 做“零拷贝接口”,返回类型根据是否需要持有决定 String 或 &‘static str

答案

“&str 与 String 的核心差异体现在所有权上:

  1. String 拥有堆分配的 UTF-8 字节缓冲区,变量是所有权值,具备 Drop 语义,离开作用域会自动释放内存;
  2. &str 只是借用视图,由胖指针(地址+长度)组成,不拥有内存,因此实现了 Copy,生命周期由编译器静态检查,禁止出现悬垂引用;
  3. 因为所有权不同,String 可以原地扩容、修改、追加,而 &str 是不可变的,任何修改都需先转为 String;
  4. 在函数签名设计时,如果调用方已有 String,传 &str 可借助Deref 强制转换避免克隆;反之若接口返回字符串,需要持有就用 String,需要静态字符串就用 &‘static str,避免生命周期标注传染给调用者。”

拓展思考

  1. 面试追问 1:如果把 &str 存进结构体,怎么做?
    答:必须显式加生命周期参数‘a,例如 struct Token<'a> { raw: &'a str },否则编译器无法证明引用有效性。
  2. 面试追问 2:String::from("abc") 与 "abc".to_string() 谁更好?
    答:二者最终都调Vec<u8>::extend_from_slice,性能一致;但团队风格统一更重要,国内大厂代码规范普遍推荐to_string(),可读性更高。
  3. 实战陷阱:在异步场景下把 &str 塞进 tokio::spawn 任务会报错,因为任务要求 'static,此时只能传 String 或 &‘static str,从而引出所有权与生命周期跨线程的约束。