在 Rust 的内存模型中,String 与 &str 是最核心的文本数据类型。它们表面上都代表字符串,但在底层实现、所有权、内存布局以及使用场景上存在根本性的差异。理解这些差异,是编写高性能、安全 Rust 程序的关键。
String 是一个 堆分配(heap-allocated) 的可变字符串类型,本质上是对 Vec<u8> 的封装。其定义大致可以理解为:
pub struct String {
vec: Vec<u8>,
}Vec<u8> 内部维护三部分数据:
因此,String 拥有对底层内存的完全控制权,可以自由扩容、修改和释放。
相比之下,&str 是对 UTF-8 字节序列的 不可变切片(immutable slice),定义上类似于:
pub struct &str {
ptr: *const u8,
len: usize,
}它只是对现有字符串的一段引用,不拥有底层数据的所有权,也不会在堆上重新分配内存。换句话说,&str 是“只读视图(read-only view)”,它更接近于 &[u8] 的高层语义封装。
String 拥有其数据的所有权,因此当它离开作用域时,内存自动释放:
{
let s = String::from("Rust");
} // s 被 drop,堆内存释放而 &str 则依赖于其引用对象的生命周期。当你从一个 String 获取切片时,Rust 编译器会自动追踪生命周期,确保引用不会悬空:
let s = String::from("Rust");
let slice: &str = &s[0..2]; // 安全:slice 生命周期不超过 s如果尝试在 s 被释放后继续使用 slice,编译器会在编译期报错。这正体现了 Rust 所有权模型的安全保障。
由于 String 涉及堆分配,创建与扩容的开销相对较高,但能灵活修改、拼接和构建动态内容。
而 &str 通常存储在编译时静态分配的二进制段(如字符串字面量)或借用自堆数据,因此访问速度快、零拷贝(zero-copy),但无法修改。
例如:
let literal: &str = "hello"; // 位于只读内存段,无需分配
let mut owned: String = String::from(literal); // 分配堆内存
owned.push_str(" world"); // 修改内容在性能敏感的场景下,如果你仅需读取数据,优先使用 &str;而当需要构造、拼接、序列化或与外部输入交互时,则必须使用 String。
一个 Rust 程序员常见的实践误区,是函数接口滥用 String 类型。例如:
fn greet(name: String) { ... } // ❌ 会迫使调用者转移所有权更好的做法是:
fn greet(name: &str) { ... } // ✅ 接受任何字符串切片这种设计兼顾灵活性与性能,使得函数既能接受字面量、String,也能接受子串,从而避免多余的堆分配与拷贝。
这也是 Rust 生态(如 std::path::Path, std::ffi::OsStr 等类型)普遍采用 “拥有类型 + 切片视图” 的模式:
String ↔ &str、Vec<T> ↔ &[T]、PathBuf ↔ &Path。
String 与 &str 的差异,不仅是语法或内存布局的不同,更是 Rust 对所有权哲学的体现:
“谁拥有数据,谁负责释放;谁只读访问,就无需承担代价。”
在实际开发中,我们应当:
&str 进行接口抽象与高效读取;
String 处理动态构建与长期持有的数据;
掌握这对核心类型的底层机制,意味着真正理解了 Rust 在内存安全与性能之间的黄金平衡点。