写 Rust 时,几乎每个新手都会在
String和&str之间反复纠结:到底什么时候用哪个?为什么&s[3..5]会 panic?String和字符串字面量又是什么关系?本文围绕 Rust 的字符串与切片,把这两者的关系、UTF-8 编码、连续 / 不连续切片以及常见报错一次性讲清楚,看完即可对齐 Rust 语言圣经「字符串与切片」这一节。
-
String(拥有所有权) --- 栈上是胖指针三件套
ptr / len / cap(共 24 字节),ptr指向堆上一段 UTF-8 缓冲区;缓冲区画成 16 格,前 13 格是有效字节rust is good!,后 3 格是「已分配未用」的空闲容量(解释为什么push_str能直接复用、避免重分配)。 -
&str 借用 String 内部 (
&s[5..9])--- 栈上只有ptr / len两个字段(没有 cap ,这正是和 String 的本质区别),ptr=0x1005指回那块堆缓冲区的中间一段并高亮显示,体现「&str 不拥有内存、生命周期受 s 约束」。 -
&str 指向字符串字面量 ---
ptr直接指向只读数据段.rodata(程序启动即常驻,如"hello"),说明为什么字面量类型就是&'static str。
对照这张图记忆要点:String = ptr+len+cap(拥有堆、可增长);&str = ptr+len(借用、无所有权)。
一、为什么 Rust 有两套字符串类型
和大多数语言「一个 string 打天下」不同,Rust 把字符串拆成了两个角色:
String:可增长、拥有所有权 的字符串,数据存放在堆 上,本质是一个保证为合法 UTF-8 的字节向量(Vec<u8>)。&str:字符串切片(视图),它只是「借来」指向某段合法的 UTF-8 字节,本身不拥有数据,不额外分配内存。
可以用一个类比来记:
| 类型 | 类比 | 所有权 | 存储位置 | 可否增长 |
|---|---|---|---|---|
String |
Vec<T> |
拥有数据 | 堆 | 可以 push |
&str |
&[T] |
借用一段 | 指向别处 | 不可以 |
也就是说,&str 之于 String,就像 &[T] 之于 Vec<T>------是对一段连续内存的只读视图。
二、创建 String 的几种方式
fn main() {
// 1. 从字符串字面量创建(推荐)
let s1 = String::from("rust is good!");
// 2. 使用 to_string(字面量调方法)
let s2 = "rust is good!".to_string();
// 3. 用类型转换
let s3: String = "rust is good!".into();
// 4. 先创建空串,再追加
let mut s4 = String::new();
s4.push_str("rust");
s4.push(' ');
s4.push_str("is good!");
}
顺带一提:当你需要批量构造若干个
String时,std::array::from_fn是非常优雅的手段。比如生成 8 个相同内容的String:
let array: [String; 8] = std::array::from_fn(|_i| String::from("rust is good!"));因为
String实现了Drop(不是Copy),不能用[String::from("..."); 8]这种语法,而from_fn会为每个位置单独调用闭包生成新值,正好绕开了这个限制。
三、String 与 &str 的相互转换
这是日常开发中最频繁的操作,记住「String 随时可以降级为 &str,反过来要重新分配内存」即可。
fn main() {
let s = String::from("hello world");
// String -> &str:三种等价写法
let a: &str = &s; // 借用,触发 Deref 自动转换
let b: &str = s.as_str(); // 显式方法
let c: &str = &s[..]; // 取整个字符串的切片
// &str -> String:需要分配新内存
let owned: String = String::from("hi");
let owned2: String = "hi".to_string();
}
函数参数上的最佳实践:参数优先写 &str ,这样 String(会自动借成 &str)和字符串字面量都能传进来,通用性最好:
fn greet(name: &str) {
println!("hello, {name}");
}
fn main() {
let s = String::from("Rust");
greet(&s); // String 自动借为 &str
greet("Course"); // 字面量本身就是 &str
greet(s.as_str()); // 显式转换
}
四、底层真相:UTF-8 变长编码
Rust 的字符串始终按 UTF-8 编码存储。UTF-8 是变长编码:英文 1 字节,部分欧洲字符 2 字节,中文通常 3 字节,少量字符 4 字节。
由此带来两个重要结论:
-
String::len()返回的是字节长度,不是字符个数; -
不能用字节下标去索引「第几个字符」------下标必须落在字符边界上,否则 panic。
fn main() {
let s = String::from("你好rust");
println!("{}", s.len()); // 11,不是 6:你好=6字节 + rust=4字节,再 +1? 见说明
// 想拿「字符个数」要用 chars().count()
println!("{}", s.chars().count());
}
说明:
你好各占 3 字节共 6 字节,rust各占 1 字节共 4 字节,所以len()是 10。字符数chars().count()才是 6。
要正确遍历内容,按需用 chars()(遍历 Unicode 字符)或 bytes()(遍历字节):
fn main() {
let s = String::from("a中b");
for c in s.chars() { println!("char: {}", c); } // a、中、b
for b in s.bytes() { println!("byte: {}", b); } // 97, 228,184,173, 98
}
五、字符串切片:连续 vs 不连续
1. 连续切片(合法)
当切片的起止下标都落在字符边界上时,得到的是一个合法的字符串切片:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
let head = &s[..5]; // "hello"(省略起始下标)
let tail = &s[6..]; // "world"(省略结束下标)
println!("{}, {}", hello, world);
}
2. 不连续切片(编译期或运行期报错)
对 String 而言,s[n..m] 切出来的类型是 str,但它是不 Sized 的切片类型 ,必须配合 & 借用以确定长度;而对 &str 直接使用切片语法且类型不匹配时会触发如下典型错误:
let s = String::from("hello");
let slice = s[3..5]; // 报错:the size for values of type `str` cannot be known
正确写法是加上 &,把它变成一个有长度的借用切片:
let s = String::from("hello");
let slice = &s[3..5]; // 正确:得到 &str
3. 边界 panic(运行期)
即便语法正确,如果切片范围不在 UTF-8 字符边界上,运行时会 panic:
fn main() {
let s = String::from("你好");
let bad = &s[0..2]; // panic:byte index 2 is not a char boundary
}
「你好」中每个字符占 3 字节,合法边界是 0、3、6;0..2 落在了「你」字内部,于是触发边界检查 panic。这是 Rust 在运行期对 UTF-8 完整性做的保护。
六、常见报错速查
| 报错信息 | 原因 | 解决方式 |
|---|---|---|
the size for values of type str cannot be known |
s[3..5] 切出的是未大小的 str |
改为 &s[3..5] 借用为 &str |
byte index N is not a char boundary |
切片下标落在 UTF-8 字符中间 | 把下标挪到合法字符边界 |
expected String, found &str |
函数要 String 却传了 &str |
用 .to_string() / String::from() 转换 |
expected &str, found String(个别场景) |
借用生命周期或类型不匹配 | 传 &s 或 s.as_str() |
七、总结
String拥有堆上的可增长数据,&str是对一段合法 UTF-8 的借用视图,两者关系如同Vec<T>与&[T]。String可无损降级为&str(&s、s.as_str()、&s[..]);反向转换需要重新分配内存。- 函数参数尽量写
&str,通用性最好。 - UTF-8 变长编码决定了
len()返回字节数,切片下标必须落在字符边界,否则 panic。 - 切片记得加
&(&s[3..5])以获得确定长度,避免str未大小的编译错误。
掌握了这套「拥有 vs 借用、字节 vs 字符、连续 vs 不连续」的心智模型,Rust 字符串这块最劝退的部分基本就被你拿下了。
如果这篇整理对你有帮助,欢迎点赞、收藏、关注,后续持续更新 Rust 基础到进阶系列。
