如何安全地将 String 切片为 &mut str?
解读
面试官抛出此题,并非考察“切一下”这么简单,而是验证候选人是否真正理解 Rust 的所有权、借用检查、UTF-8 边界、别名可变性规则四大核心约束。国内大厂(如华为、阿里、字节)的 Rust 岗面试中,“String ↔ str” 转换安全是高频深挖点,答不好会被追问到 Pin、UnsafeCell、UB 场景,直接决定能否拿到“安全编码”维度的 A 档评分。
知识点
- String 与 str 的内存布局:String 拥有堆缓冲区并带容量字段;str 只是 [u8] 的 UTF-8 视图,无所有权信息。
- Rust 别名规则:同一作用域内,&mut str 必须保证独占访问,任何同时存在的 &str 或 &mut str 都不能重叠。
- UTF-8 边界对齐:字节索引必须落在字符边界(char boundary),否则
slice_mut_unchecked会产生 UB。 - 标准库安全 API:
String::as_mut_str()→ 拿到整个 &mut str;str::get_mut(range)→ 返回Option<&mut str>,内部做边界+UTF-8 检查;unsafe str::get_unchecked_mut(range)→ 跳过检查,需自建安全抽象。
- 自持有切片(self-referential)陷阱:把 &mut str 存到结构体再返回,极易违反借用检查,需要
unsafe或ouroboros等库,但面试中不建议炫技,除非能给出完整安全契约。
答案
安全切片的最简、最稳、最符合国内面试官预期的写法只有两步:
- 用
String::as_mut_str()拿到整个 &mut str; - 用
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。
拓展思考
- 多线程场景:把 &mut str 装进
Mutex<String>后,锁粒度是整段缓冲区,若需更细粒度可改用RwLock<Vec<char>>或rope结构;直接放 &mut str 到线程外会违反 Send 边界。 - 零拷贝解析协议:在网络高性能框架(如国内飞书 Rust 网关)中,常把
BytesMut先freeze成Bytes,再用str::get_mut原地改写包头;此时需保证所有并发读取端已降级为 &str,否则触发数据竞争 UB。 - unsafe 抽象封装:若必须极致性能(内核 virtio 场景),可用
unsafe封装一个StrMut<'a>,在构造函数里一次性检查边界并记录长度,后续操作省略检查;但需给出完整安全文档,包括:- 生命周期 'a 必须严格短于原 String;
- 禁止同时存在任何 &str 到同一区间;
- 提供
fn into_bytes(self) -> &'a mut [u8]时重新声明 UTF-8 不变式。
国内面试中,能讲清契约比会写 unsafe 更加分。