String 字符串延申
为什么 str 是 DST?
我们可以从计算机内存分配的角度,用一个极简的例子拆解为什么 str 是 DST(动态大小类型)。
编译器的困惑
假设 Rust 允许你直接定义 str 类型,请看这段代码:
// 假设这段代码能跑通
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 时,奇迹发生了:
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 | &str | String |
|---|---|---|---|
| 能作为普通变量 | ❌ | ✅ | ✅ |
| 拥有所有权 | ❌ | ❌ | ✅ |
| 运行时动态创建(如用户输入) | ❌ | ❌ | ✅ |
| 追加、修改内容 | ❌ | ❌ | ✅ |
| 无需关心原始数据的生命周期 | ❌ | ❌ | ✅ |
// 只读借用,适合函数参数和模式匹配
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、&str 与 String 的区别
str是原始字节序列,是 DST(动态大小类型),无法直接用作变量类型&str是对str的引用,是胖指针(Fat Pointer),包含地址和长度String是可变的字符串类型,是包装了Vec<u8>的结构体,其指向的就是str数据
字符串的本质是
u8数组。str是裸字符串切片(数据本身),&str是它的引用(指向数据的胖指针),String则是堆上拥有所有权的字符串。
| 特性 | str(裸字符串切片) | &str(字符串切片引用) | String(字符串对象) |
|---|---|---|---|
| 类型本质 | 原生字节序列(Unsized) | 指向 str 的指针(胖指针) | 包装了 Vec<u8> 的结构体 |
| 内存位置 | 数据可在字面量区、堆或栈上 | 栈(存放指针和长度) | 栈(存放元数据)+ 堆(存放内容) |
| 大小是否固定 | 不固定(DST),编译时无法确定 | 固定(2 个字长: 地址 + 长度) | 固定(3 个字长: 地址+长度+容量) |
| 所有权 | 不拥有所有权 | 借用所有权 | 拥有所有权 |
| 可变性 | 无法直接操作 | 通常不可变 | 可变(通过 push、insert 等) |
| 生命周期 | 取决于数据源(字面量是 'static) | 受其指向的数据源限制 | 离开作用域时自动释放(Drop) |
| 常用场景 | 作为泛型参数或底层表示 | 函数参数、匹配模式、切片操作 | 数据存储、动态拼接、修改内容 |
let s: &str = "hello"; // 字符串字面量,类型为 &'static str
let s2: String = s.to_string(); // &str → String(堆拷贝)
let s3: &str = &s2; // String → &str(借用)字面量的 'static 生命周期
字符串字面量的类型是 &'static str,'static 表示该引用在整个程序运行期间都有效。这是因为字面量在编译期被硬编码写入二进制文件,程序加载时放入全局字面量区,永远不会被释放:
let s: &'static str = "hello"; // 显式标注(通常省略)
let s: &str = "hello"; // 等价写法,生命周期被省略推导当把字面量传入需要 'static 生命周期的场景(如线程间传递、全局变量)时,这一特性十分重要:
// 只有 &'static str 才能存入 static 变量
static GREETING: &str = "Hello, Rust!"; // ✅ 字面量满足 'static
let s = String::from("hello");
static BAD: &str = s.as_str(); // ❌ 编译错误: s 的生命周期不是 'static