一句话总结
String 是拥有所有权,可增长的堆上字符串;&str 是指向一段 UTF-8 字节的只读"视图".
前者负责持有数据,后者负责借用数据--这正是 Rust 所有权模型在字符串上的投影.
从一个问题出发:为什么有两种字符串
初学者最常见的困惑是:明明都是字符串,为什么函数签名一会儿写 String,一会儿写 &str?换成其他语言,一个 string 类型就够用了.
答案藏在 Rust 的核心约束里:每一份堆上数据都必须有唯一的所有者,负责在恰当时机释放它.字符串内容长度不定,必须存在堆上,于是 Rust 把"持有这段数据"和"临时查看这段数据"拆成了两个类型.
String 像是买下并持有的一栋房子,拥有钥匙,能改建,最终负责拆除.
&str 则是一张参观通行证,可以进去看看,但既不能拆墙,也不需要在离开时负责拆房.
内存模型:三个字 vs 两个字
理解二者差异,最直接的方式是看它们在栈上的布局.假设有这样一段代码:
|
String 在栈上占据三个机器字:指向堆内存的指针,长度(len),容量(capacity).真正的字节 h e l l o 存放在堆上.
&str 是一个胖指针(fat pointer),在栈上占据两个机器字:指向某段 UTF-8 字节的指针,以及这段字节的长度.它自己不拥有任何堆内存,只是"借看"别处的数据.
下图展示了上面两个变量的内存关系:
|
capacity 的意义
String 比 &str 多出的 capacity 字段,记录的是"已申请但未必用满"的堆空间.这让 String 能像动态数组那样追加内容而不必每次都重新分配--这也是它能"增长"的原因.&str 只读,自然不需要容量信息.
为什么几乎只见 &str,很少见 str
str 本身是一个类型,但几乎从不直接使用,原因是它属于动态大小类型(Dynamically Sized Type,DST).编译器在编译期无法确定一个 str 到底占多少字节,因此不能把它直接放在栈上,也不能作为普通变量或函数参数按值传递.
解决办法是给它套一层指针.&str 就是"指向 str 的引用",而引用的大小是编译期已知的(指针 + 长度),于是一切都成立了.
str 好比"一段长度未知的录音",没法把它整个塞进一个固定大小的盒子.
&str 则是"这段录音的起点位置 + 时长",用这两个已知信息就能精确定位它.
字符串字面量是什么类型
代码里直接写出的 "hello",类型是 &'static str.它指向的字节被编译器直接嵌入到程序的只读数据段,与程序同生共死,所以生命周期是 'static.
|
字面量不是 String
很多从其他语言转来的开发者会写 let mut s = "hello"; s += " world";,然后被编译器拦住.字面量是 &str,只读且大小固定.需要可变,可增长的字符串时,必须显式构造 String,例如 String::from("hello") 或 "hello".to_string().
关键机制:解引用强制转换
一个常见的场景是把 String 传给接收 &str 参数的函数,却能通过编译:
|
这依赖一个叫解引用强制转换(Deref coercion) 的机制.String 实现了 Deref<Target = str>,编译器在类型不匹配时会自动把 &String 转成 &str.本质是:String 内部就持有一段 UTF-8 字节,取它的 &str 视图是零成本的--只是把堆指针和长度拷给一个胖指针,不复制任何字节.
函数参数的经验法则
定义函数参数时优先用 &str 而非 &String.&str 能同时接受字符串字面量,String 的借用,以及其他字符串切片,适用面更广;而 &String 只能接受 String 的借用,白白收窄了调用方的选择.
该用哪个:决策清单
| 场景 | 选择 | 理由 |
|---|---|---|
| 函数只读取字符串内容 | &str |
不需要所有权,接受面最广 |
| 结构体需要长期持有字符串 | String |
拥有数据,不受外部生命周期牵制 |
| 需要修改,拼接,增长内容 | String |
只有它可变且可增长 |
| 返回新构造的字符串 | String |
函数结束后数据需继续存活 |
| 返回输入字符串的一部分 | &str |
切片复用原数据,零拷贝 |
结构体字段慎用 &str
在结构体里放 &str 字段会强制引入生命周期标注(如 struct User<'a> { name: &'a str }),意味着这个结构体不能比它借用的字符串活得更久,使用起来处处受限.除非确有零拷贝的强需求,否则结构体字段一般直接用 String,让它自己拥有数据.
两者之间如何转换
|
方向不对称的代价
&str → String 必然伴随堆分配 + 字节复制,因为要创造一份新的,被拥有的数据;而 String → &str 只是生成一个指向已有数据的视图,零成本.在热点路径上频繁调用 to_string() 是常见的性能浪费点.
快速回顾
- 所有权分工:
String拥有堆数据,&str只借用,对应 Rust 的所有权与借用模型 - 内存布局:
String是 ptr+len+cap 三个字,&str是 ptr+len 两个字的胖指针 - str 是 DST:大小编译期未知,必须通过
&str这层引用使用 - 字面量是
&'static str,只读,嵌入程序,随程序存活 - 参数优先
&str,靠解引用强制转换自动兼容&String - 下一篇将深入 UTF-8 编码,解释为什么 Rust 不让用
s[i]取字符
动手练习
- first_word 函数:写一个
fn first_word(s: &str) -> &str,返回第一个空格前的子串.用字面量和String两种实参分别调用,验证两者都能传入. - 结构体生命周期:尝试定义
struct User { name: &str }(不加生命周期),观察编译器报错信息,再改为带生命周期标注的版本和直接用String的版本,对比两者的使用差异. - 栈上大小验证:用
std::mem::size_of::<String>()和std::mem::size_of::<&str>()打印两种类型在栈上的大小,验证"三个字 vs 两个字"的结论.