首页 / Rust 入门教程 / 字符串切片与所有权

Rust 入门教程

字符串切片与所有权

本教程共 78 篇 · 第 25 篇 · 更新于 2026-08-08 · 约 10 分钟阅读

RustRust 入门教程字符串切片String&str所有权UTF-8

本节目标:分清 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是(需 mutString::from.to_string()

25-9 结构体字段与字符串拼接

String&str 在结构体字段里也常出现。比如一个配置项既可能是写死的字面量(&str),也可能是运行时读入的内容(String)。如果字段用 &str,你得处理”这个引用借自谁、能活多久”(生命周期),新手阶段先用 String 最省心:

struct Config {
    name: String,   // 拥有,简单
    // name: &str,  // 会要求生命周期注解,暂时别用
}

拼接字符串也是常见操作。Stringpush_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(自动解引用,免费);&strStrings.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)“,学习把多个相关字段打包成自定义类型。