Skip to content

String 字符串延申

为什么 str 是 DST?

我们可以从计算机内存分配的角度,用一个极简的例子拆解为什么 strDST(动态大小类型)

编译器的困惑

假设 Rust 允许你直接定义 str 类型,请看这段代码:

rust
// 假设这段代码能跑通
fn main() {
  let a: str = "hi";        // 这段数据占 2 个字节
  let b: str = "rustacean"; // 这段数据占 10 个字节
}

问题出在"栈内存分配"上:

在 Rust 中,函数内部的局部变量是存在栈(Stack)上的。编译器在编译代码时,必须确切知道每个变量需要多少栈空间,而且同样类型的变量必须占用相同的栈空间。

  • 如果 a 是 str,编译器要分 2 字节。
  • 如果 b 是 str,编译器要分 10 字节。

对于编译器来说,此时的 str 类型没有一个固定的大小。但编译器又必须保证每个相同类型的变量占用相同固定的栈空间。这就导致了矛盾。

这就是 DST 的定义: 只要一个类型在编译时无法确定具体占多少字节,它就是 DST(动态大小类型)

为什么 &str 不是 DST?

当你加上 & 变成 &str 时,奇迹发生了:

rust
let s1: &str = "hi";
let s2: &str = "rustacean";

&str 是一个胖指针(Fat Pointer),它包含了两个部分:

  • 一个指向字符串内容的地址 (通常是 8 字节)
  • 一个表示字符串长度的字段 (通常也是 8 字节)

无论字符串内容有多长,&str 的大小都是固定的 (16 字节)。编译器在编译时就能确定 &str 的大小,所以它不是 DST。

为什么 String 不是 DST?

String 是一个结构体,它在栈上占用固定的空间(通常是 24 字节 超胖指针),包含了指向堆上字符串数据的指针、字符串长度和容量。无论字符串内容有多长,String 在栈上的大小都是固定的,所以它也不是 DST。

栈上的 String 变量(固定 24 字节)
┌──────────┬──────────┬──────────┐
│  ptr     │  len     │  cap     │
│ (8字节)  │ (8字节)  │ (8字节)  │
└────┬─────┴──────────┴──────────┘

     ▼ 堆上(大小不固定,运行时动态分配)
  ┌──────────────────────┐
  │  h  e  l  l  o  ... │
  └──────────────────────┘

字符串内容存在上,内容有多长,堆上就分多少空间。但栈上的 String 变量本身只保存指向堆的指针(ptr)、已用长度(len)和已分配容量(cap),这三个字段的大小始终固定,编译器一眼就能算出来。

String 本质就是为了解决 str 作为 DST 无法直接使用的问题而设计的一个结构体,它在栈上提供了一个固定大小的句柄(Handle)来管理堆上的字符串数据,并提供了丰富的方法来操作字符串内容。

总结

  • str 是那串具体的"字节内容",由于内容长短不一,所以它是 DST(没法直接放变量里)。
  • &str 是一个描述这串内容的"中介",它有固定的大小,所以我们平时用的都是它。
  • String 是一个更高级的"管理员",它不仅描述内容,还拥有内容,并且能修改内容,它的大小也是固定的。

为什么需要 String

str 是 DST,不能直接当变量;&str 虽然能用,但只能借用且通常不可变。这两者都无法满足"在运行时动态创建、修改字符串"的需求,所以需要 String:

需求str&strString
能作为普通变量
拥有所有权
运行时动态创建(如用户输入)
追加、修改内容
无需关心原始数据的生命周期
rust
// 只读借用,适合函数参数和模式匹配
fn greet(name: &str) {
    println!("Hello, {}!", name);
}

// 需要构建、修改或返回字符串时,必须用 String
fn build_greeting(name: &str) -> String {
    let mut s = String::from("Hello, ");
    s.push_str(name);
    s.push('!');
    s // 所有权转移给调用者
}

实践原则: 函数接收字符串时优先用 &str(更通用,兼容 String 的借用);函数需要返回或持有字符串时用 String

str&strString 的区别

  • str 是原始字节序列,是 DST(动态大小类型),无法直接用作变量类型
  • &str 是对 str 的引用,是胖指针(Fat Pointer),包含地址和长度
  • String 是可变的字符串类型,是包装了 Vec<u8> 的结构体,其指向的就是 str 数据

字符串的本质是 u8 数组。str 是裸字符串切片(数据本身),&str 是它的引用(指向数据的胖指针),String 则是堆上拥有所有权的字符串。

特性str(裸字符串切片)&str(字符串切片引用)String(字符串对象)
类型本质原生字节序列(Unsized)指向 str 的指针(胖指针)包装了 Vec<u8> 的结构体
内存位置数据可在字面量区、堆或栈上栈(存放指针和长度)栈(存放元数据)+ 堆(存放内容)
大小是否固定不固定(DST),编译时无法确定固定(2 个字长: 地址 + 长度)固定(3 个字长: 地址+长度+容量)
所有权不拥有所有权借用所有权拥有所有权
可变性无法直接操作通常不可变可变(通过 pushinsert 等)
生命周期取决于数据源(字面量是 'static)受其指向的数据源限制离开作用域时自动释放(Drop)
常用场景作为泛型参数或底层表示函数参数、匹配模式、切片操作数据存储、动态拼接、修改内容
rust
let s: &str = "hello";          // 字符串字面量,类型为 &'static str
let s2: String = s.to_string(); // &str → String(堆拷贝)
let s3: &str = &s2;             // String → &str(借用)

字面量的 'static 生命周期

字符串字面量的类型是 &'static str'static 表示该引用在整个程序运行期间都有效。这是因为字面量在编译期被硬编码写入二进制文件,程序加载时放入全局字面量区,永远不会被释放:

rust
let s: &'static str = "hello"; // 显式标注(通常省略)
let s: &str = "hello";         // 等价写法,生命周期被省略推导

当把字面量传入需要 'static 生命周期的场景(如线程间传递、全局变量)时,这一特性十分重要:

rust
// 只有 &'static str 才能存入 static 变量
static GREETING: &str = "Hello, Rust!"; // ✅ 字面量满足 'static

let s = String::from("hello");
static BAD: &str = s.as_str();         // ❌ 编译错误: s 的生命周期不是 'static

基于 MIT 协议发布