如何安全地将 String 切片为 &mut str?

解读

面试官抛出此题,并非考察“切一下”这么简单,而是验证候选人是否真正理解 Rust 的所有权、借用检查、UTF-8 边界、别名可变性规则四大核心约束。国内大厂(如华为、阿里、字节)的 Rust 岗面试中,“String ↔ str” 转换安全是高频深挖点,答不好会被追问到 Pin、UnsafeCell、UB 场景,直接决定能否拿到“安全编码”维度的 A 档评分。

知识点

  1. String 与 str 的内存布局:String 拥有堆缓冲区并带容量字段;str 只是 [u8] 的 UTF-8 视图,无所有权信息
  2. Rust 别名规则:同一作用域内,&mut str 必须保证独占访问,任何同时存在的 &str 或 &mut str 都不能重叠。
  3. UTF-8 边界对齐:字节索引必须落在字符边界(char boundary),否则 slice_mut_unchecked 会产生 UB。
  4. 标准库安全 API
    • String::as_mut_str() → 拿到整个 &mut str;
    • str::get_mut(range) → 返回 Option<&mut str>内部做边界+UTF-8 检查
    • unsafe str::get_unchecked_mut(range) → 跳过检查,需自建安全抽象。
  5. 自持有切片(self-referential)陷阱:把 &mut str 存到结构体再返回,极易违反借用检查,需要 unsafeouroboros 等库,但面试中不建议炫技,除非能给出完整安全契约。

答案

安全切片的最简、最稳、最符合国内面试官预期的写法只有两步:

  1. String::as_mut_str() 拿到整个 &mut str;
  2. str::get_mut(range) 做子切片,利用标准库已封装好的边界+UTF-8 检查

代码示例:

fn safe_slice_mut(s: &mut String, range: impl std::ops::RangeBounds<usize>) -> Option<&mut str> {
    // 1. 先拿到整个可变视图
    let full = s.as_mut_str();
    // 2. 用标准库安全 API 做子切片
    full.get_mut(range)
}

该实现:

  • 零 unsafe 代码,编译期即可通过 Miri 检测;
  • 自动拒绝非字符边界,返回 None 而非 UB;
  • 不破坏 String 所有权,调用者仍可通过原变量继续修改。

若面试官追问“如何自己实现边界检查”,可补充:

fn check_char_boundary(s: &str, i: usize) -> bool {
    if i > s.len() { return false; }
    s.is_char_boundary(i)
}

并强调:任何手动 slice_unchecked 前必须同时验证 i <= s.len()s.is_char_boundary(i),否则即 UB。

拓展思考

  1. 多线程场景:把 &mut str 装进 Mutex<String> 后,锁粒度是整段缓冲区,若需更细粒度可改用 RwLock<Vec<char>>rope 结构;直接放 &mut str 到线程外会违反 Send 边界
  2. 零拷贝解析协议:在网络高性能框架(如国内飞书 Rust 网关)中,常把 BytesMutfreezeBytes,再用 str::get_mut 原地改写包头;此时需保证所有并发读取端已降级为 &str,否则触发数据竞争 UB
  3. unsafe 抽象封装:若必须极致性能(内核 virtio 场景),可用 unsafe 封装一个 StrMut<'a>,在构造函数里一次性检查边界并记录长度,后续操作省略检查;但需给出完整安全文档,包括:
    • 生命周期 'a 必须严格短于原 String;
    • 禁止同时存在任何 &str 到同一区间;
    • 提供 fn into_bytes(self) -> &'a mut [u8]重新声明 UTF-8 不变式
      国内面试中,能讲清契约比会写 unsafe 更加分