Skip to content

字符串 String 延申

str &strString 区别

  • str 是字符串字面量,是DST(动态大小类型)
  • &str 是字符串切片,是胖指针(Fat Pointer)
  • String 是可变的字符串类型,是包装了 Vec<u8> 的结构体

字符串的本质是 u8 数组,str 是字符串切片,&str 是字符串切片引用,String 是可变的字符串类型

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

let s4 = String::from("hello"); // String
let s5 = &s4; // &str
println!("{}", s5); // hello

为什么 str 是 DST?

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

高级类型 章节对 DST 会有更详细的介绍

1. 编译器的困惑

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

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

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

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

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

对于编译器来说,str 这个类型本身没有一个固定的体型.它像是一块可以无限拉长或缩短的橡皮筋.编译器没法在编译阶段说:"所有的 str 统一占 X 字节".

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

2. 为什么 &str 却不是 DST?

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

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

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

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

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

3. 形象类比:电影 vs 电影海报

  • str 是"电影本身": 有的电影 90 分钟,有的电影 4 小时.如果你问:"一场电影占用多少磁盘空间?"没有固定答案.这就是 DST.
  • &str 是"电影海报(带二维码)": 不管电影多长,海报的大小都是固定的(一张纸).海报上印着电影在哪看(地址)以及电影时长(长度).你可以在手里(栈上)拿很多张海报,因为它们大小一致.

总结

  • str 是那串具体的"字节内容",由于内容长短不一,所以它是 DST(没法直接放变量里).
  • &str 是一个描述这串内容的"管理员",它有固定的大小,所以我们平时用的都是它.

基于 MIT 协议发布