字符串
Rust 的字符串底层数据形式是: UTF-8 编码的字节序列,数据以 [u8] 数组的形式存储在内存中。
Rust 有两种字符串类型: str 和 String,它们都是基于 UTF-8 编码的字节序列构建的,但在使用方式和内存管理上有显著区别。
str类型是字符串数据本体,也是String类型的切片类型,通常以引用形式&str出现。String类型可以看作str类型的容器,是由标准库提供,封装了Vec<u8>的结构体,支持可增长、可变、可拥有的 UTF-8 编码字符串。
String 是一个结构体,内部包含一个 Vec<u8> 字段,用于存储 str 数据:
// 简化后的概念结构
pub struct String {
vec: Vec<u8>, // Vec<u8> 必须始终保持 UTF-8 合法,这里的 vec 可以视为 str
}
String和str的关系就像 碗 和 饭,String是str的拥有者和容器,str是具体的字符串内存数据。
str、&str 与 String 的关系
Rust 需要一种方式来表示"一串连续的文本",于是使用了 [u8] 数组。
但是 [u8] 数组可能是任何字节序列,有可能是图片二进制、乱码、甚至是任意不合法的字节流,不一定是有效的 UTF-8 编码。为了确保字符串数据的有效性,Rust 定义了 str 类型,它针对 [u8] 字节数组进行了类型安全包装(Type Wrapper),保证了数据的正确性和安全性。
从底层内存来看,str 的布局与 [u8] 类似,本质上都是连续字节数据;但 str 额外要求这些字节必须始终满足 UTF-8 编码规则。
因此 str 可以看作符合 UTF-8 编码的 [u8] 字节序列,其实就是合法字符串数据的本体,它代表的是一个连续的文本内容,而不是任意的字节流。
但 str 是 DST(动态大小类型),大小只有运行时才能确定,编译时无法为它分配固定的栈空间,所以不能直接作为变量使用。为了能操作字符串,Rust 提供了两种方案:
String: 通过结构体在堆上持有这块str数据,拥有所有权,支持增长和修改。&str: 用一个胖指针借用这块str数据,只读,不拥有所有权。
str(字符串数据的本体,DST)
/ \
String &str
拥有 str 数据 借用 str 数据
存在堆上,可增长修改 只读引用,64位平台是 16 字节理解三者的关键在于: str 是数据本体,String 和 &str 都是围绕它构建的。
推荐先阅读 字符串延申 章节,深入理解
str、&str和String的关系,再回来看这个章节会更容易。
字面量类型(&str)
由于 str 是 DST(动态大小类型),不能直接作为变量使用,所以通常以引用形式 &str 出现
定义语法: let var_name: &str = "string literal";
字符串字面量使用双引号包围,其类型被推导为 &str:
let s = "hello"; // 推导为 &str
let s: &str = "hello"; // 等价写法,显式指定类型字面量的存储原理
字符串字面量(&str)并不是先在堆上创建一个 String,再引用它来得到 &str 的。编译器对字符串字面量(&str)做了特殊处理:
- 编译期: 编译器将字符串字面量(
&str)以硬编码的方式写入程序二进制文件中 - 加载期: 程序被加载时,字符串字面量(
&str)被放入内存的全局字面量区(不在堆上,也不在栈上) - 运行期: 执行到
let s = "hello"时,将全局字面量区中该数据的地址保存到栈上的&str变量s中
二进制文件 程序内存(加载后) 变量 s(栈上)
┌──────────┐ ┌──────────────┐ ┌───────────────────┐
│ "hello" │ → │ "hello" │ ←── │ ptr + len(5) │
│ (硬编码) │ │(全局字面量区) │ │ &str │
└──────────┘ └──────────────┘ └───────────────────┘因此,字符串字面量(
&str)拥有 'static 生命周期——程序运行期间始终有效,这正是字面量类型为&str而非str的原因之一。
char 与字面量的区别
| 特性 | char | &str |
|---|---|---|
| 声明符号 | 单引号 '' | 双引号 "" |
| 存储内容 | 单个字符 | 字符序列 |
| 大小 | 固定 4 字节 | 固定指针大小 |
| 类型性质 | 原始类型 | 引用类型 |
| Unicode 表示 | 单个码位 | 多个码位序列 |
String 类型
String 类型没有 &str 类型对应的字面量构建方式,只能通过方法构建:
| 方法 | 定义语法 | 说明 |
|---|---|---|
new() | String::new() | 创建空字符串 |
from() | String::from("hello") | 从 &str 字面量构建 |
to_string() | "hello".to_string() | 从 &str 转换 |
to_owned() | "hello".to_owned() | 从 &str 转换(显式拷贝) |
let mut s = String::new(); // 新建可变空字符串
let s = String::from("hello"); // 从字面量构建
let s = "hello".to_string(); // 等价写法
let s = "hello".to_owned(); // 等价写法String 堆拷贝原理
let s = String::from("hello");这行代码的执行过程: 编译器将 "hello" 硬编码写入二进制文件,程序加载时放入全局字面量区;运行到 String::from() 时,从字面量区将内容拷贝到堆内存;栈上的变量 s 保存指向堆内存的指针、长度和容量(共 3 个字长)。
全局字面量区 堆内存 变量 s(栈上)
┌──────────┐ ┌──────────┐ ┌──────────────────────────┐
│ "hello" │ → │ "hello" │ ←── │ ptr + len(5) + cap(5) │
└──────────┘ └──────────┘ │ String │
(从字面量区拷贝而来) └──────────────────────────┘String 独有方法
String 独有一系列用于修改内容、管理内存和所有权转换的方法:
let mut s = String::from("hello world");
// 追加
s.push('!'); // 追加单个字符(char 类型)
s.push_str(" Rust"); // 追加 &str,不转移参数所有权
// 删除与修改
s.pop(); // 移除并返回最后一个字符 → Option<char>
s.remove(0); // 按字节索引移除字符(必须位于字符边界)
s.insert(0, 'H'); // 在指定字节位置插入字符
s.insert_str(0, "Hi, "); // 在指定字节位置插入 &str
s.retain(|c| c.is_alphabetic()); // 原地保留满足条件的字符
s.truncate(5); // 截断到指定字节长度(必须位于字符边界,否则 panic)
s.clear(); // 清空内容(保留已分配内存)
// 容量管理
s.capacity(); // 已分配的堆内存字节数 → usize
s.reserve(10); // 确保至少有额外 n 字节的可用空间
s.shrink_to_fit(); // 释放多余容量,使 capacity == len
s.shrink_to(5); // 缩减容量到不小于指定值
// 所有权转换
s.as_str(); // 借用为 &str(不转移所有权)
s.into_bytes(); // 消耗 String,返回底层 Vec<u8>
s.drain(1..3); // 移除指定字节范围的字符并返回迭代器
s.split_off(5); // 从字节位置截断:原串保留前部,后部作为新 String 返回公共常用方法
以下方法定义在 str 上。由于 String 实现了 Deref 隐式转换,&str 和 String 均可直接调用。
方法分类
查询
查询字符串属性或内容,返回相关信息,不修改原字符串,不转移所有权。
let s = " Hello, Rust! "; // &str 或 String 均可
s.len(); // 字节数(非字符数) → usize
s.is_empty(); // 是否为空 → bool
s.chars().count(); // Unicode 字符数(UTF-8 安全) → usize
s.contains("Rust"); // 是否包含子串/字符 → bool
s.starts_with(" Hello"); // 是否以指定前缀开头 → bool
s.ends_with(" "); // 是否以指定后缀结尾 → bool
s.find("Rust"); // 首次匹配的字节位置 → Option<usize>
s.rfind("l"); // 最后匹配的字节位置 → Option<usize>
s.is_ascii(); // 是否全为 ASCII 字符 → bool迭代
借用原数据,迭代字符串内容,返回一个迭代器,每次迭代返回一个元素(如字符、字节、行等),元素类型取决于具体方法。
let s = " Hello, Rust! "; // &str 或 String 均可
s.chars(); // 按 Unicode 字符迭代 → char
s.bytes(); // 按字节迭代 → u8
s.char_indices(); // 按 (字节偏移, 字符) 迭代 → (usize, char)
s.lines(); // 按换行符分行迭代 → &str
s.split(','); // 按分隔符分割 → &str
s.split_whitespace(); // 按连续空白符分割(忽略空段) → &str
s.splitn(2, ','); // 最多分割为 n 段 → &str修剪
修剪字符串首尾或指定模式,返回一个新的 &str 切片,借用原数据,不修改原字符串。
let s = " Hello, Rust! "; // &str 或 String 均可
s.trim(); // 去除首尾空白
s.trim_start(); // 去除首部空白
s.trim_end(); // 去除尾部空白
s.trim_matches('x'); // 去除首尾与模式匹配的字符
s.trim_start_matches(" "); // 去除首部与模式匹配的部分
s.trim_end_matches(" "); // 去除尾部与模式匹配的部分转换
转换字符串内容,返回一个新的 String,不修改原字符串,不转移原字符串所有权。
let s = " Hello, Rust! "; // &str 或 String 均可
s.to_uppercase(); // → String
s.to_lowercase(); // → String
s.replace("Rust", "World"); // 替换全部匹配 → String
s.replacen("l", "L", 2); // 只替换前 n 个 → String
s.repeat(3); // 重复拼接 n 次 → String切片
切片字符串内容,返回一个新的 &str 切片,借用原数据,不修改原字符串,不转移原字符串所有权。
let s = " Hello, Rust! "; // &str 或 String 均可
let _ = &s[0..5]; // 直接切片(需在字符边界,越界 panic)
s.get(0..5); // 安全切片 → Option<&str>
s.split_at(5); // 按字节位置拆分为两段 → (&str, &str)字符串转其他类型
字符串转换为其他类型,返回一个新的值,不修改原字符串,不转移原字符串所有权。需要目标类型实现了 FromStr trait。
let s = " Hello, Rust! "; // &str 或 String 均可
let n: i32 = "42".parse().unwrap();
let f: f64 = "3.14".parse().unwrap();
// 推荐: 使用 parse::<T>() 显式指定类型,避免依赖上下文推断
let n = "42".parse::<i32>().unwrap();字符串索引与切片
字面量 &str 和 String 不支持直接整数索引,只能通过切片访问,因为 Rust 的字符串是 UTF-8 编码,一个字符可能占 1 到 4 个字节,整数索引的语义不明确:
let s = "hello";
let c = s[0]; // ❌ 编译错误: 不支持直接整数索引使用切片时,必须在字符边界处切割,否则会 panic:
let s = "Здравствуйте"; // 西里尔字母,每个字符 2 字节
let slice = &s[0..4]; // ✅ 正确: 4 字节 = 两个字符
println!("{}", slice); // Зд
let slice = &s[0..3]; // ❌ 运行时 panic: 在字符中间切割获取字符串长度
len()返回字节数;chars().count()返回 Unicode 字符数(UTF-8 安全)。
let s = "你好";
println!("{}", s.len()); // 6(每个汉字 3 字节)
println!("{}", s.chars().count()); // 2含多字节字符时用
chars().count()获取字符数
遍历字符串
chars(): 按 Unicode 字符迭代,返回char类型,推荐用于字符处理bytes(): 按原始字节迭代,返回u8类型,用于字节级操作
for c in "Зд".chars() {
println!("{c}");
}
// 输出: З
// д
for b in "Зд".bytes() {
println!("{b}");
}
// 输出: 208 151 208 180需要人能理解的字符时,使用
chars();需要机器处理的字节时,使用bytes()。
字符串拼接
+ 运算符
+ 底层调用 add() 方法,第一个参数接管 self 的所有权,必须是 String 类型,第二个参数为 &str:
标准库定义:
// 简化后的概念结构
impl Add<&str> for String {
type Output = String;
fn add(self, rhs: &str) -> String { ... }
}使用示例:
let s1 = String::from("Hello, ");
let s2 = "world!"; // 这是 &str
// 1. 正确:String + &str
let s3 = s1 + s2;
// 2. 错误:&str 不能放在 + 的左边
let s4 = "Hello, " + "world!"; // ❌ no implementation for `&str + &str`format! 宏
format! 不获取任何所有权,适合多个字符串拼接:
let r1: &str = "tic";
let r2: &str = "tac";
let s3: String = String::from("toe");
// 无论 &str 还是 String,都能直接放入
let s = format!("{}-{}-{}", r1, r2, s3);
// r1, r2, s3 之后依然可以使用
println!("{}", r1);
format!使用和println!完全相同的格式语法,但不打印,而是返回一个String
| 方式 | 所有权 | 性能 | 适用场景 |
|---|---|---|---|
+ | 转移第一个参数 | 较高(直接追加) | 简单两段拼接 |
format! | 不转移任何参数 | 稍低(额外分配) | 多段拼接、格式化 |
&str 与 String 互转
&str → String
let s = "hello"; // &str
// 1. 从字面量构建 String
let s2 = String::from(s);
// 2. 使用 to_string 方法,常用
let s1 = s.to_string();
// 3. 使用 to_owned 方法,等价于 to_string,但更显式地表示"获取所有权"
let s3 = s.to_owned();String → &str
let owned = String::from("hello"); // String
// 1. 直接借用为 &str,不转移所有权
let borrowed: &str = &owned;
// 2. 使用 as_str 方法,等价于直接借用,但更显式地表示"获取 &str 切片"
let borrowed2: &str = owned.as_str();