在 Rust 的世界里,字符串远不止是“文本”的代名词。它们体现了所有权、生命周期与内存安全的哲学。而最常见的两种类型 —— String&str —— 乍看相似,却在实现层面有着本质区别。理解它们的内部机制,不仅能帮助我们写出高性能的代码,也能更好地理解 Rust 的核心设计理念。


一、核心差异:堆分配与借用视角

在 Rust 中,String 是一个 拥有所有权(owned) 的字符串类型;而 &str 是一个 借用的字符串切片(borrowed slice)

  • String 是一个可增长、可变的、拥有底层内存所有权的结构。
  • &str 是对 UTF-8 编码字节序列的只读视图,不拥有数据。

这意味着:

  • String 通常存储在 堆上(heap),其结构中保存了指向堆内存的指针、长度与容量。
  • &str 则是一个 胖指针(fat pointer),包含一个数据指针与一个长度字段,仅描述一段只读的内存。

二、内部实现揭秘

我们可以通过 std::mem::size_of::<T>() 来窥探它们的内部结构差异:

fn main() {
    use std::mem::size_of;
    println!("size_of::<String> = {}", size_of::<String>());
    println!("size_of::<&str> = {}", size_of::<&str>());
}

在 64 位系统下,输出通常为:

size_of::<String> = 24
size_of::<&str> = 16

解释如下:

  • String = 指针(8字节) + 长度(8字节) + 容量(8字节)
    结构体大致可抽象为:

    struct String {
        ptr: *mut u8,  // 指向堆内存
        len: usize,    // 当前字符串长度
        cap: usize,    // 已分配容量
    }
    
  • &str = 指针(8字节) + 长度(8字节)
    它只是对某段 UTF-8 内存的不可变引用:

    struct &str {
        ptr: *const u8,
        len: usize,
    }
    

换言之,String 拥有实际的内存,而 &str 只是描述。


三、从 &strString:内存所有权的转移

一个常见的操作是将字符串字面量(&'static str)转换为可变字符串:

fn main() {
    let s1: &str = "Rust";  // 静态内存
    let mut s2: String = s1.to_string(); // 分配到堆上
    s2.push_str(" rocks!");
    println!("{}", s2);
}

解释:

  • "Rust" 是编译期已知的常量,存放在 只读静态内存区
  • to_string() 会在堆上为 s1 分配新的空间;
  • push_str() 修改了堆内存中的数据,因此需要所有权。

如果我们反过来,把 String 转为 &str

fn main() {
    let s = String::from("Hello");
    let slice: &str = &s; // 借用,不发生拷贝
    println!("{}", slice);
}

这里没有内存分配,slice 只是引用了 s 的数据。Rust 编译器通过生命周期检查确保 sslice 失效前不会被释放。


四、底层内存布局:一图胜千言

String(堆分配):
 ┌────────────┐      ┌──────────────────────┐
 │ ptr  ──────┼────>│ 72 75 73 74 (UTF-8) │
 │ len = 4    │      └──────────────────────┘
 │ cap = 8    │
 └────────────┘

&str(借用切片):
 ┌────────────┐
 │ ptr  ──────┼────> same data (只读视图)
 │ len = 4    │
 └────────────┘

这说明:

  • String 负责分配与释放;
  • &str 只是“看一眼”数据;
  • Rust 的借用系统保证 &str 不会悬垂引用已释放的内存。

五、实践中的性能与设计思考

理解这些实现细节对性能优化极为重要:

  1. 避免不必要的堆分配
    当只需读取时,使用 &str
    当需修改时,再转为 String

  2. 函数参数的优雅设计
    接口建议使用 impl AsRef<str>,同时兼容 String&str

    fn greet<S: AsRef<str>>(name: S) {
        println!("Hello, {}!", name.as_ref());
    }
    

    这样可避免多余的 to_string()

  3. 内存安全背后的哲学
    Rust 通过区分 String&str,在语义层面绑定了内存所有权与生命周期。
    开发者无需手动管理释放,编译器会在类型系统中保证安全。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐