字符串切片与所有权
本教程共 78 篇 · 第 25 篇 · 更新于 2026-08-08 · 约 10 分钟阅读
本节目标:分清
String(拥有、可变、堆上)与&str(借用、不可变、可能是字面量)的所有权差异,学会函数优先接收&str,并避开 UTF-8 字节索引的坑。
字符串是日常编程里最高频的类型,也最容易让新手在”所有权”上犯晕。Rust 里有两个主力字符串类型:String 和 &str。这一章把它们的关系彻底讲清。先给你一个震撼的事实:在很多别的语言里 "hello" 就是”字符串”的全部,但在 Rust 里,光是字符串就能整出好几种类型,搞不清它们,编译器的报错能让你怀疑人生。
25-1 字符串字面量是 &str
你最早写的 println!("Hello, world!"); 里那个字符串,就是字面量。声明一个字面量:
let s = "hello"; // s 的类型是 &str
s 的类型是 &str(读作 string slice,字符串切片)。它指向已编译进程序二进制文件里的一段不可变文本。&str 不拥有这段数据——数据在程序里一直都在,s 只是借来看。所以字面量字符串既不能改、也不需要谁去释放它。这也是为什么字面量永远安全:它的命和整个程序一样长,不存在”被释放”的问题。
25-2 String 拥有数据
String 是另一种:它在堆上动态分配、拥有数据、可以增长修改:
let mut s = String::from("hello");
s.push_str(", world"); // 可以改,因为 s 拥有堆上的数据
println!("{s}");
String::from("hello") 在堆上开出空间存 “hello”,s 是所有者。因为拥有,所以能改;因为拥有,s 离开作用域时数据被释放。你可以把它想象成:字面量是刻在墙上的字(改不了、也丢不了),String 是你亲手写在便签上、随时能涂改的那张。
Note一句话记:
String是”房东(拥有、可变、堆上)“,&str是”路标/租客(借来看、不可变)“。字面量天然是&str,而&String也能退化成&str。两者的关系,就是”拥有者”和”借用者”的关系。
25-3 从 String 切出 &str
String 可以借用出一段,得到 &str:
let s = String::from("hello world");
let hello: &str = &s[0..5]; // "hello"
println!("{hello}");
&s[0..5] 是对 s 前 5 个字符的切片,类型 &str。原 String 仍是 owner,切片只是借看一段。这就是上一章说的”字符串切片是切片家族一员”。注意 &s[0..5] 这里按字节算(英文一个字符一字节,所以刚好 5 个字符);一旦涉及中文,这个写法就危险了,详见后面。
25-4 函数优先接收 &str
写接收字符串的函数时,用 &str 做参数最通用,因为它既能接收字面量、也能接收 String 的借用:
fn greet(name: &str) {
println!("你好,{name}");
}
fn main() {
greet("小明"); // 字面量 &str,直接传
let s = String::from("小红");
greet(&s); // String 借成 &str 传
}
如果参数写成 String,那字面量 "小明" 就传不进去(类型不匹配),得先 String::from 包一下、还把所有权交出去,调用方之后就不能用了。所以函数只读字符串时,参数用 &str 是铁律级最佳实践。它像一根能同时插进两种插座的转接头:字面量、String 都能喂进去。
Tip口诀:函数”只是读一下字符串”→ 参数用
&str;函数”要拥有并改造它”→ 参数用String(或&mut String)。前者覆盖 90% 场景。养成这个习惯,你的函数复用性会高很多。
25-5 想要 String 怎么办:to_owned / to_string
有时函数收了 &str,内部却需要一份自己的、可修改的 String:
fn make_shout(s: &str) -> String {
let mut owned = s.to_string(); // 或 s.to_owned()
owned.push('!');
owned
}
to_string() / to_owned() 会复制出一份 String,归函数所有、可随意改。这是把 &str 转成拥有型 String 的标准做法。代价是复制了一份数据,但换来了”函数内部自由修改、且不和调用方纠缠所有权”。
25-6 UTF-8 字节索引陷阱
这是字符串切片最坑的一点。Rust 字符串是 UTF-8 编码,一个字符可能占 1~4 字节。切片索引按字节算,不是按”字符个数”算:
let s = "你好";
// let bad = &s[0..1]; // 运行时 panic!截了半个汉字
“你”在 UTF-8 里占 3 字节,&s[0..1] 只取了第 0 字节,不构成完整字符,Rust 直接运行时报错,绝不让你产生乱码。这其实是 Rust 在保护你——宁可崩溃,也不给你一段”半个字”的垃圾数据。要安全地取子串,用针对字符的方法:
let s = "你好世界";
let first: String = s.chars().take(2).collect(); // 取前 2 个字符 -> "你好"
chars() 按 Unicode 字符迭代,而不是字节,安全。处理含中文等非 ASCII 文本时,永远用字符级 API,别用字节索引去切。
Warning凡是
&s[a..b]切含有非英文字符的字符串,都要警惕”截到半个字符”。日常处理用户输入、中文文本,优先chars()、.split()、.lines()这类按字符/语义切分的方法,避免字节切片。这条规矩踩一次就够你记一辈子。
25-7 为什么 Rust 字符串不能用下标取字符
你可能在别的语言里写过 s[0] 拿第一个字符。在 Rust 里 let h = s1[0]; 会编译报错——String 根本没实现”用整数下标取字符”。原因有两层:
第一,字符串底层是字节数组(Vec<u8>)。中文里 "中国人" 实际占 9 个字节(每字 3 字节),s[0] 取到的是第一个字节,根本不是完整的”中”字,这种返回值毫无意义。第二,Rust 希望下标访问是 O(1) 常数时间,但 UTF-8 字符串要”数到第几个字符”得从头遍历,无法保证 O(1)。所以 Rust 干脆不让这种含糊的操作存在,逼你显式地选择”我要字节还是我要字符”。
Note想要字符就用
s.chars()遍历;想要字节就用s.bytes()遍历。两个 API 把”字符视角”和”字节视角”彻底分开,你每次用都清楚自己在看哪一层。这种”把模糊操作变显式”的设计哲学,是 Rust 一贯的风格。
25-8 一个对照总结
| 类型 | 是否拥有数据 | 可修改 | 存在哪 | 典型来源 |
|---|---|---|---|---|
&str | 否(借用) | 否 | 字面量在二进制/或借自 String | "hello"、切片 |
String | 是 | 是(需 mut) | 堆 | String::from、.to_string() |
25-9 结构体字段与字符串拼接
String 和 &str 在结构体字段里也常出现。比如一个配置项既可能是写死的字面量(&str),也可能是运行时读入的内容(String)。如果字段用 &str,你得处理”这个引用借自谁、能活多久”(生命周期),新手阶段先用 String 最省心:
struct Config {
name: String, // 拥有,简单
// name: &str, // 会要求生命周期注解,暂时别用
}
拼接字符串也是常见操作。String 用 push_str 追加 &str,或用 + 运算符(注意 + 左边是 String、右边是 &str,且会移动左边):
let mut s = String::from("你好");
s.push_str(",世界");
let s2 = String::from("!");
let combined = s + &s2; // "你好,世界!"
+ 要求右边是 &str、左边是 String,写 s + s2(两个都是 String)会报错,要写成 s + &s2。这是新手易错点,记住加号两边类型不对等。另外拼接多个、或混着变量排版时,用 format! 更舒服:
let name = "小明";
let age = 18;
let info = format!("{} 今年 {} 岁", name, age); // 返回 String
format! 用法和 println! 一样,但它把结果返回成 String,不打印。多个字符串拼一起时它比 + 清爽,也不会产生一堆中间所有权转移。
Warning处理用户输入、文件、网络来的文本时,里面可能含中文等多字节字符。凡是要”截取子串”,务必走
chars()、split()、lines()等字符级 API,绝不用&s[a..b]按字节切,否则极易截出半截字符导致运行时 panic。这条在讲字符串切片时就强调过,这里再提醒一遍。
25-10 String 与 &str 选择速查
实际写代码时,经常纠结字段/参数该用 String 还是 &str。给个速查:如果这个值”由你创建、要长期持有、可能修改”,用 String;如果”只是借用别人传来的、只读一下”,用 &str。函数参数几乎总是 &str(兼容字面量和 String 借用),返回需要交出的新字符串时用 String。
Tip判断口诀:数据”归我管”用
String,数据”借来用”用&str。写结构体字段不确定时先用String(简单、不用管生命周期),学完生命周期再优化成&str。这是避免早期心智负担的务实选择。
Warning别为了省一次拷贝而在所有地方用
&str,结果到处要处理生命周期标注。新手阶段宁可多几次to_string()换来代码简单,也不要过早优化成满屏'a。性能瓶颈通常不在这一两次字符串复制上。
25-11 字符串拼接与转换的实操
知道了 String 和 &str 的区别,拼字符串就不慌了。要在 String 后面追加内容,用 push_str("...")(接 &str)或 push('字')(接单个 char);不要用 +,它容易让类型对新手困惑(+ 会”吃掉”左边的 String 所有权)。String 和 &str 互转:String 转 &str 用 &s(自动解引用,免费);&str 转 String 用 s.to_string() 或 String::from(s)(会复制一份到堆上)。
Tip拼接多个片段时,与其反复
push_str,不如用format!宏一次性组装:let msg = format!("你好,{},你第{}次来", name, n);。format!返回String,用法和println!几乎一样,只是不打印、而是产出字符串。它在生成动态文本时非常好用。
记住:&str 不能修改、不能增长;一旦你需要”改”或”攒”字符串,就先把它变成 String(比如 let mut s = base.to_string();),攒完再按需借出 &str。这样把”可变性”和”视图”分工清楚,代码就不会和借用检查器打架。
25-12 小结
&str 是不拥有、不可变的字符串视图(字面量天然是它);String 拥有、可改、在堆上。函数只读时用 &str 最通用。切含非 ASCII 文本务必走字符级 API,别用字节索引。到这,所有权的四大主角(移动、引用、可变引用、切片)讲完。下一章进入”结构体(struct)“,学习把多个相关字段打包成自定义类型。