字符串类型
> 理解 &str(字符串切片)与 String(堆分配字符串)的本质区别及相互转换。
String 和 &str(重点难点)
这是 Rust 初学者最容易困惑的地方,我们详细讲解。
String vs &str 对比
┌─────────────────────────────────────────────────────────┐
│ String vs &str 对比 │
├─────────────────────────────────────────────────────────┤
│ │
│ String │
│ ├── 可增长的字符串 │
│ ├── 所有权在堆上 │
│ ├── 可以修改内容 │
│ ├── 大小:24 字节(指针 + 长度 + 容量) │
│ └── 字面量创建:String::from("hello") │
│ │
│ &str │
│ ├── 字符串切片/视图 │
│ ├── 借用数据,不拥有所有权 │
│ ├── 通常不可变 │
│ ├── 大小:16 字节(指针 + 长度) │
│ └── 字面量创建:"hello"(类型是 &'static str) │
│ │
└─────────────────────────────────────────────────────────┘内存布局可视化
字符串字面量 "hello"(类型:&'static str):
┌─────────────────────────────────────────────────┐
│ 栈(Stack) │
│ ┌───────────────┬───────────────┐ │
│ │ 指针 │ 长度 │ ← &str │
│ │ (指向堆) │ (5) │ │
│ └───────┬───────┴───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 静态区(.rodata) │ │
│ │ "hello\0" │ │
│ │ ^ 字符串存储在这里 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 特点:编译时确定,整个程序生命周期存在 │
└─────────────────────────────────────────────────┘
String(可增长字符串):
┌─────────────────────────────────────────────────┐
│ 栈(Stack) │
│ ┌───────────────┬───────────────┬─────────┐ │
│ │ 指针 │ 长度 │ 容量 │ │
│ │ (指向堆) │ (5) │ (10) │ │
│ └───────┬───────┴───────────────┴─────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 堆(Heap) │ │
│ │ "hello" │ 空闲空间 │ │ │
│ │ └──────┘ └─────────────┘ │ │
│ │ 已用 (5) 可用 (5) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 特点:运行时分配,可以增长 │
└─────────────────────────────────────────────────┘基本用法
rust
▶ Runfn main() {
// &str - 字符串字面量(不可变)
let s1: &str = "hello";
// String - 可增长字符串
let s2: String = String::from("hello");
let s3: String = "hello".to_string();
let s4: String = "hello".to_owned();
println!("s1 (str): {}", s1);
println!("s2 (String): {}", s2);
println!("s3 (String): {}", s3);
println!("s4 (String): {}", s4);
// 类型验证
println!("s1 类型:{:?}", std::any::type_name_of_val(&s1));
println!("s2 类型:{:?}", std::any::type_name_of_val(&s2));
}修改 String
rust
▶ Runfn main() {
let mut s = String::from("hello");
// 追加字符串
s.push_str(" world");
println!("追加后:{}", s);
// 追加单个字符
s.push('!');
println!("追加字符:{}", s);
// 拼接
let s1 = String::from("hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意:s1 被移动,s2 仍然可用
println!("拼接:{}", s3);
// println!("s1 = {}", s1); // ❌ 错误,s1 已移动
// 使用 format! 宏(不移动所有权)
let s1 = String::from("hello");
let s2 = String::from("world");
let s3 = format!("{} {}", s1, s2);
println!("format: {}", s3);
println!("s1 仍可用:{}", s1);
println!("s2 仍可用:{}", s2);
}字符串切片(Deref coercion)
rust
▶ Runfn main() {
let mut s = String::from("hello world");
// 获取 String 的切片
let slice: &str = &s[0..5]; // "hello"
println!("切片:{}", slice);
// 自动解引用(Deref coercion)
// &String 可以自动转换为 &str
fn takes_str(s: &str) {
println!("收到:{}", s);
}
let my_string = String::from("hello");
takes_str(&my_string); // &String → &str
takes_str(my_string.as_str()); // 显式转换
takes_str("literal"); // 字面量就是 &str
}String 和&str 转换
rust
▶ Runfn main() {
// &str → String
let s1: &str = "hello";
let s2: String = s1.to_string();
let s3: String = s1.to_owned();
let s4: String = String::from(s1);
// String → &str
let string = String::from("hello");
let str1: &str = &string;
let str2: &str = string.as_str();
// 函数参数
fn print_string(s: &String) {
println!("String: {}", s);
}
fn print_str(s: &str) {
println!("str: {}", s);
}
let my_string = String::from("hello");
print_string(&my_string); // 需要 &String
print_str(&my_string); // &String 自动转为 &str
print_str("literal"); // 字面量是 &str
print_str(&my_string[0..3]); // 切片是 &str
}常见陷阱
rust
▶ Runfn main() {
// 陷阱 1:试图修改&str
let mut s: &str = "hello";
// s.push_str(" world"); // ❌ &str 没有 push_str 方法
// 正确做法:使用 String
let mut s: String = String::from("hello");
s.push_str(" world");
println!("{}", s);
// 陷阱 2:悬垂引用
// fn get_string() -> &String {
// let s = String::from("hello");
// &s // ❌ 错误:返回局部变量的引用
// }
// 正确做法:返回 String
fn get_string() -> String {
String::from("hello")
}
let owned = get_string();
println!("{}", owned);
// 陷阱 3:索引访问中文字符
let s = String::from("你好");
// let first = s[0]; // ❌ 错误!不能通过索引访问
// 正确做法:使用字符迭代
let first_char = s.chars().next().unwrap();
println!("第一个字符:{}", first_char);
// 字节长度 vs 字符长度
println!("字节数:{}", s.len()); // 6 (每个中文 3 字节)
println!("字符数:{}", s.chars().count()); // 2
}实战:字符串处理
rust
▶ Runfn main() {
let text = String::from(" Hello, Rust! ");
// 去除空白
let trimmed = text.trim();
println!("去空白:'{}'", trimmed);
// 大小写转换
println!("大写:{}", trimmed.to_uppercase());
println!("小写:{}", trimmed.to_lowercase());
// 替换
let replaced = trimmed.replace("Rust", "World");
println!("替换:{}", replaced);
// 分割
let parts: Vec<&str> = trimmed.split(", ").collect();
println!("分割:{:?}", parts);
// 查找
if trimmed.contains("Rust") {
println!("包含 Rust");
}
if trimmed.starts_with("Hello") {
println!("以 Hello 开头");
}
if trimmed.ends_with("!") {
println!("以!结尾");
}
// 查找位置
if let Some(pos) = trimmed.find("Rust") {
println!("Rust 在位置:{}", pos);
}
}小结
&str:字符串字面量/切片,存储在只读内存,不可变,零拷贝String:堆分配的可增长字符串,可变&str→String:.to_string()或String::from()String→&str:&s或s.as_str()- Rust 字符串是 UTF-8 编码,不支持按字节索引
练习题
详见:练习题