Rust: Rust 字符串全面解析:&str 与 String 的奥秘
最后更新:2026-08-26
Rust 的字符串类型不是一种,而是两种——因为系统编程需要精确控制内存。
Rust 的字符串处理是很多初学者的困惑点:为什么有 String 和 &str 两种?这和 C 语言的 char* 有什么不同?本课带你彻底搞懂。
1. 你将学到
- 字符串字面量
&str和堆分配字符串String的区别 String的创建、追加、拼接和格式化- 字符串切片(slicing)和索引访问
- 字符串的常用方法(len/contains/replace/trim)
- UTF-8 编码对字符串操作的影响
2. 一位图书管理员的故事
(1) 痛苦:JSON 数据太长放不下
Maria 是一名图书管理员,她在开发一个小型图书检索系统来管理 5,000 本藏书。
- 每本书的书名长度不同,最长的有 200 个字符
- 最开始她用固定长度字符串存书名(C 语言的
char[50]) - 结果"《百年孤独》"能放进去,但"《How to Make a Complete Map of an Infinite Universe》"被截断了
- 改用
char*又遇到内存释放问题——改了一个地方忘记 free,系统崩溃
"如果有自动管理内存的字符串该多好……"她后来在 Rust 里找到了答案。
(2) Rust 的字符串方案
RUST
fn main() {
// &str:字符串字面量,编译时就存在二进制里
let title_book1: &str = "百年孤独";
// String:堆分配的,可变长,自动管理内存
let mut title_book2 = String::from("How to Make a Complete Map of an Infinite Universe");
title_book2.push_str(" (Second Edition)"); // 追加内容
println!("书 1: {} (长度: {} 字节)", title_book1, title_book1.len());
println!("书 2: {} (长度: {} 字节)", title_book2, title_book2.len());
// String 离开作用域时自动释放,不需要手动 free
}
Rust 的
String像 Java 的字符串——自动管理内存;&str像 C 的const char*——只读引用。两者配合,兼顾性能和灵活性。
3. &str 和 String 的区别
(1) 内存模型
graph LR
subgraph "&str (字符串字面量)"
S1_ptr[ptr ──→ ...rust...]
S1_len[len: 4]
end
subgraph "String (堆分配)"
S2_ptr[ptr ──→ Rust is...]
S2_len[len: 10]
S2_cap[cap: 16]
end
subgraph "存储位置"
STACK[栈: &str<br>只读借用]
HEAP[堆: String<br>拥有所有权]
end
S1_ptr -.->|编译时确定| STACK
S2_ptr -.->|运行时分配| HEAP
| 特性 | &str |
String |
|---|---|---|
| 所有者 | 只读借用 | 拥有所有权 |
| 可变性 | 不可变 | 可追加/修改 |
| 存储位置 | 只读数据段(编译时) | 堆(运行时分配) |
| 创建方式 | 字面量 "hello" |
String::from() 或 .to_string() |
| 适用场景 | 只读的字符串参数 | 需要修改或拥有字符串所有权 |
(2) String 常用方法速查
| 方法 | 返回类型 | 说明 | 示例 |
|---|---|---|---|
push_str(&str) |
() |
追加字符串切片 | s.push_str("abc") |
push(char) |
() |
追加单个字符 | s.push('!') |
len() |
usize |
返回字节长度 | s.len() |
is_empty() |
bool |
是否为空 | s.is_empty() |
trim() |
&str |
去除两端空白 | s.trim() |
contains(&str) |
bool |
是否包含子串 | s.contains("rust") |
replace(&str, &str) |
String |
替换所有匹配 | s.replace("a", "b") |
split(char) |
Split |
按分隔符分割 | s.split(',') |
to_lowercase() |
String |
转小写 | s.to_lowercase() |
to_uppercase() |
String |
转大写 | s.to_uppercase() |
chars() |
Chars |
按字符遍历 | s.chars() |
bytes() |
Bytes |
按字节遍历 | s.bytes() |
clear() |
() |
清空字符串 | s.clear() |
capacity() |
usize |
返回缓冲区容量 | s.capacity() |
(3) 创建方式
RUST
// &str 创建
let s1: &str = "hello, world"; // 字符串字面量
let s2: &str = r"raw string\n"; // 原始字符串(不转义)
// String 创建
let s3 = String::from("hello");
let s4 = "world".to_string();
let s5 = format!("{}-{}", s3, s4); // 格式化创建
let s6: String = "hi".into(); // 通过类型转换
(4) 字符串转换方式速查
| 源类型 | 目标类型 | 转换方式 | 是否零开销 |
|---|---|---|---|
&str |
String |
"hello".to_string() |
分配堆内存 |
&str |
String |
String::from("hello") |
分配堆内存 |
&str |
String |
"hello".into() |
分配堆内存 |
String |
&str |
&s 或 s.as_str() |
零开销(借用) |
String |
&str |
&s[..] |
零开销(切片) |
&str |
&[u8] |
"hello".as_bytes() |
零开销(借用) |
String |
Vec<u8> |
s.into_bytes() |
零开销(转移所有权) |
Vec<u8> |
String |
String::from_utf8(v) |
零开销(转移所有权,需验证 UTF-8) |
4. 字符串常见操作
▶ 示例 1:String 的创建与修改(难度 ⭐)
RUST
// ============================================
// String 的创建、追加和拼接
// ============================================
fn main() {
// 创建
let mut s = String::from("Hello");
// 追加
s.push_str(", "); // 追加字符串切片
s.push('R'); // 追加单个字符
s.push_str("ust!");
println!("追加后: {}", s);
// 拼接(+ 操作符会消费左侧 String)
let s1 = String::from("Hello, ");
let s2 = String::from("World!");
let s3 = s1 + &s2; // s1 被移动,不能再用了
println!("拼接后: {}", s3);
// format! 宏(不消费任何变量)
let a = String::from("tic");
let b = String::from("tac");
let c = String::from("toe");
let result = format!("{}-{}-{}", a, b, c);
println!("format 结果: {}", result);
}
输出:
TEXT
📖 仅展示
追加后: Hello, Rust!
拼接后: Hello, World!
format 结果: tic-tac-toe
+操作符会消费(move)左侧的String,所以s1在拼接后不能再使用。format!不会消费任何变量,所有变量都保留所有权。
▶ 示例 2:字符串切片与索引(难度 ⭐⭐)
RUST
// ============================================
// 字符串切片——注意 UTF-8 边界!
// ============================================
fn main() {
let s = String::from("Rust编程");
// 获取字符串长度(字节数,不是字符数)
println!("字节长度: {}", s.len()); // 10(4 字节 ASCII + 6 字节 UTF-8 中文)
// 字符串切片(按字节索引,且必须在字符边界上)
let slice1 = &s[0..4]; // "Rust"(前 4 字节正好是一个完整字符边界)
println!("前 4 字节切片: {}", slice1);
// &s[0..5] 会 panic!(因为 4-5 字节是中文的第一个字节中间)
// 按字符数遍历
for c in s.chars() {
print!("{} ", c);
}
println!();
// 按字节遍历
for b in s.bytes() {
print!("{:02x} ", b);
}
println!();
}
输出:
TEXT
📖 仅展示
字节长度: 10
前 4 字节切片: Rust
R u s t 编 程
52 75 73 74 e7 bc 96 e7 a8 8b
Rust 字符串不支持按索引直接访问(如
s[0]),因为 UTF-8 的字符是变长的。必须用.chars()遍历字符或用字节切片。切片必须落在字符边界上,否则运行时崩溃。
▶ 示例 3:字符串常用方法(难度 ⭐⭐)
RUST
// ============================================
// 字符串常用的查询、替换、裁剪方法
// ============================================
fn main() {
let s = " Hello, Rust World! ";
// 去除两端空白
println!("trim: '{}'", s.trim());
// 包含判断
println!("contains 'Rust': {}", s.contains("Rust"));
// 替换
let replaced = s.replace("Rust", "Go");
println!("replace: '{}'", replaced);
// 分割
let parts: Vec<&str> = "apple,banana,cherry".split(',').collect();
println!("split: {:?}", parts);
// 大小写转换
let lower = "HELLO".to_lowercase();
let upper = "world".to_uppercase();
println!("lower: {}, upper: {}", lower, upper);
}
输出:
TEXT
📖 仅展示
trim: 'Hello, Rust World!'
contains 'Rust': true
replace: ' Hello, Go World! '
split: ["apple", "banana", "cherry"]
lower: hello, upper: WORLD
trim()和split()返回的是&str,不创建新字符串。replace()返回全新的String。需要修改时考虑性能开销。
▶ 示例 4:UTF-8 字符串遍历与边界检查(难度 ⭐⭐⭐)
RUST
// ============================================
// UTF-8 字符串的字节、字符和图素簇
// ============================================
fn main() {
let s = "Rust🦀你好";
println!("字符串: {}", s);
println!("字节长度: {}", s.len());
println!("字符数量: {}", s.chars().count());
println!("\n--- 逐字符遍历 ---");
for (i, c) in s.chars().enumerate() {
println!("字符 {}: '{}' (Unicode: U+{:04X})", i, c, c as u32);
}
println!("\n--- 逐字节遍历 ---");
for (i, b) in s.bytes().enumerate() {
print!("{:02x} ", b);
if (i + 1) % 8 == 0 { println!(); }
}
println!();
println!("\n--- 安全切片 ---");
let ascii_part = &s[0..4];
println!("ASCII 部分 &s[0..4]: '{}'", ascii_part);
let emoji_start = 4;
let emoji_end = emoji_start + 4;
let emoji_part = &s[emoji_start..emoji_end];
println!("Emoji 部分 &s[{}..{}]: '{}'", emoji_start, emoji_end, emoji_part);
let chinese_start = emoji_end;
let chinese_end = chinese_start + 6;
let chinese_part = &s[chinese_start..chinese_end];
println!("中文部分 &s[{}..{}]: '{}'", chinese_start, chinese_end, chinese_part);
let combined = format!("{}{}{}", ascii_part, emoji_part, chinese_part);
println!("拼接还原: {}", combined);
}
输出:
TEXT
📖 仅展示
字符串: Rust🦀你好
字节长度: 14
字符数量: 7
--- 逐字符遍历 ---
字符 0: 'R' (Unicode: U+0052)
字符 1: 'u' (Unicode: U+0075)
字符 2: 's' (Unicode: U+0073)
字符 3: 't' (Unicode: U+0074)
字符 4: '🦀' (Unicode: U+1F980)
字符 5: '你' (Unicode: U+4F60)
字符 6: '好' (Unicode: U+597D)
--- 逐字节遍历 ---
52 75 73 74 f0 9f a6 80
e4 bd a0 e5 a5 bd
--- 安全切片 ---
ASCII 部分 &s[0..4]: 'Rust'
Emoji 部分 &s[4..8]: '🦀'
中文部分 &s[8..14]: '你好'
拼接还原: Rust🦀你好
UTF-8 中 ASCII 字符占 1 字节,中文占 3 字节,emoji 占 4 字节。切片必须落在字符边界上,否则运行时 panic。使用
.chars().enumerate()可以安全地按字符处理。
▶ 示例 5:字符串解析与格式化实战(难度 ⭐⭐)
RUST
// ============================================
// 字符串解析、格式化和连接的综合运用
// ============================================
fn main() {
let raw_data = "name=Alice;age=30;city=Beijing";
println!("原始数据: {}", raw_data);
println!("\n--- 解析键值对 ---");
let pairs: Vec<&str> = raw_data.split(';').collect();
for pair in pairs {
let parts: Vec<&str> = pair.split('=').collect();
if parts.len() == 2 {
let key = parts[0].trim();
let value = parts[1].trim();
println!("{} => {}", key, value);
}
}
println!("\n--- 构建新字符串 ---");
let name = "Alice";
let age = 30;
let city = "Beijing";
let summary = format!("{},{} 岁,来自 {}", name, age, city);
println!("摘要: {}", summary);
let mut report = String::from("人员报告\n");
report.push_str(&format!("姓名: {}\n", name));
report.push_str(&format!("年龄: {}\n", age));
report.push_str(&format!("城市: {}\n", city));
report.push_str("--- 结束 ---");
println!("\n{}", report);
let csv_line = ["Alice", "30", "Beijing"].join(",");
println!("\nCSV 格式: {}", csv_line);
}
输出:
TEXT
📖 仅展示
原始数据: name=Alice;age=30;city=Beijing
--- 解析键值对 ---
name => Alice
age => 30
city => Beijing
--- 构建新字符串 ---
摘要: Alice,30 岁,来自 Beijing
人员报告
姓名: Alice
年龄: 30
城市: Beijing
--- 结束 ---
CSV 格式: Alice,30,Beijing
split()+collect()是解析结构化文本的基本模式;format!适合一次性拼接;push_str()适合逐步构建长文本;join()适合用分隔符连接数组。
❓ 常见问题
Q 为什么 Rust 要有两种字符串类型?
A 所有权和性能的平衡。
Q String 和 &str 怎么相互转换?
A &str → String 用
.to_string() 或 String::from();String → &str 用 &s 或 s.as_str()。Q 为什么 s[0] 不能取到第一个字符?
A 因为 UTF-8 是变长编码。
Q 字符串拼接用 + 还是 format!?
A 少量拼接用 +,多个片段用 format!。
Q 字符串可以修改吗?&str 和 String 谁能改?
A 只有
String 可以修改(mut 声明后可用 .push_str()、.push()、.insert() 等方法)。&str 是只读借用,不能修改指向的数据。📖 小节
- Rust 有两种字符串类型:
&str(只读借用)和String(拥有所有权) String是堆分配的、可增长的 UTF-8 字符串,离开作用域自动释放&str是字符串切片,指向一段连续的有效 UTF-8 字节- UTF-8 编码导致字符串不支持按索引直接访问,需用
.chars()或字节切片 - 常用方法:
.len()、.trim()、.contains()、.replace()、.split() - 函数参数接收字符串时尽量用
&str以获得最大灵活性
📝 作业
- 难度 ⭐:创建一个
String,依次调用.push_str()、.push()、+操作符和format!,对比四种方式的结果。 - 难度 ⭐⭐:写一个函数
fn first_word(s: &str) -> &str,返回字符串的第一个单词(按空格分割)。分别用&str和String调用它。 - 难度 ⭐⭐⭐:探索 UTF-8 边界问题——创建一个包含 emoji 🦀 的字符串
"Rust🦀",尝试对它做&s[0..5]和&s[0..6]切片,观察哪个成功哪个崩溃。