Rust 中的字符串与切片(String 与 &str)

写 Rust 时,几乎每个新手都会在 String 和 &str 之间反复纠结:到底什么时候用哪个?为什么 &s[3..5] 会 panic?String 和字符串字面量又是什么关系?

本文围绕 Rust 的字符串与切片,把这两者的关系、UTF-8 编码、连续 / 不连续切片以及常见报错一次性讲清楚,看完即可对齐 Rust 语言圣经「字符串与切片」这一节。

  1. String(拥有所有权) --- 栈上是胖指针三件套 ptr / len / cap(共 24 字节),ptr 指向堆上一段 UTF-8 缓冲区;缓冲区画成 16 格,前 13 格是有效字节 rust is good!,后 3 格是「已分配未用」的空闲容量(解释为什么 push_str 能直接复用、避免重分配)。

  2. &str 借用 String 内部 (&s[5..9])--- 栈上只有 ptr / len 两个字段(没有 cap ,这正是和 String 的本质区别),ptr=0x1005 指回那块堆缓冲区的中间一段并高亮显示,体现「&str 不拥有内存、生命周期受 s 约束」。

  3. &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 字节。

由此带来两个重要结论:

  1. String::len() 返回的是字节长度,不是字符个数;

  2. 不能用字节下标去索引「第几个字符」------下标必须落在字符边界上,否则 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()

七、总结

  1. String 拥有堆上的可增长数据,&str 是对一段合法 UTF-8 的借用视图,两者关系如同 Vec<T> 与 &[T]。
  2. String 可无损降级为 &str(&s、s.as_str()、&s[..]);反向转换需要重新分配内存。
  3. 函数参数尽量写 &str,通用性最好。
  4. UTF-8 变长编码决定了 len() 返回字节数,切片下标必须落在字符边界,否则 panic。
  5. 切片记得加 &(&s[3..5])以获得确定长度,避免 str 未大小的编译错误。

掌握了这套「拥有 vs 借用、字节 vs 字符、连续 vs 不连续」的心智模型,Rust 字符串这块最劝退的部分基本就被你拿下了。


如果这篇整理对你有帮助,欢迎点赞、收藏、关注,后续持续更新 Rust 基础到进阶系列。

相关推荐
Tisfy1 小时前
LeetCode 3863.将一个字符串排序的最小操作次数:分类讨论(一次遍历)——简单易懂版
算法·leetcode·字符串·题解·分类讨论
Young Doro1 小时前
从平衡表征到最小充分因果表征:端到端神经网络因果效应估计
算法
0+1112 小时前
算法 --递归
算法·leetcode
打工仔折腾 AI2 小时前
介入式脑机接口的跨尺度解码与闭环调控:从算法落地到后端工程实践
人工智能·后端·python·深度学习·算法·ai agent 实战
凤山老林2 小时前
授权模型只会RBAC?深入 ABAC 授权模型,实现权限判断做成策略引擎
大数据·数据库·算法
海绵宝宝转agent3 小时前
2026-10-21学习笔记
笔记·学习·算法
Dxy12393102163 小时前
ShardingSphere 自定义分片算法:BINARY(16) UUIDv7 哈希分片实现
算法·哈希算法
罗湖老棍子14 小时前
【例 1】Sightseeing Trip(信息学奥赛一本通- P1494)(洛谷-P10927)
数据结构·算法·图论·最短路·floyd·最小环
stsdddd16 小时前
YOLO算法建筑工地安全帽目标检测数据集16881张:标注核对笔记
算法·yolo·目标检测