Skip to content

字符串

Rust 的字符串底层数据形式是: UTF-8 编码的字节序列,数据以 [u8] 数组的形式存储在内存中。

Rust 有两种字符串类型: strString,它们都是基于 UTF-8 编码的字节序列构建的,但在使用方式和内存管理上有显著区别。

  • str 类型是字符串数据本体,也是 String 类型的切片类型,通常以引用形式 &str 出现。
  • String 类型可以看作 str 类型的容器,是由标准库提供,封装了 Vec<u8> 的结构体,支持可增长、可变、可拥有的 UTF-8 编码字符串。

String 是一个结构体,内部包含一个 Vec<u8> 字段,用于存储 str 数据:

rust
// 简化后的概念结构
pub struct String {
    vec: Vec<u8>, //  Vec<u8> 必须始终保持 UTF-8 合法,这里的 vec 可以视为 str
}

Stringstr 的关系就像 碗 和 饭,Stringstr 的拥有者和容器, str 是具体的字符串内存数据。

str&strString 的关系

Rust 需要一种方式来表示"一串连续的文本",于是使用了 [u8] 数组。

但是 [u8] 数组可能是任何字节序列,有可能是图片二进制、乱码、甚至是任意不合法的字节流,不一定是有效的 UTF-8 编码。为了确保字符串数据的有效性,Rust 定义了 str 类型,它针对 [u8] 字节数组进行了类型安全包装(Type Wrapper),保证了数据的正确性和安全性。

从底层内存来看,str 的布局与 [u8] 类似,本质上都是连续字节数据;但 str 额外要求这些字节必须始终满足 UTF-8 编码规则。

因此 str 可以看作符合 UTF-8 编码的 [u8] 字节序列,其实就是合法字符串数据的本体,它代表的是一个连续的文本内容,而不是任意的字节流。

strDST(动态大小类型),大小只有运行时才能确定,编译时无法为它分配固定的栈空间,所以不能直接作为变量使用。为了能操作字符串,Rust 提供了两种方案:

  • String: 通过结构体在上持有这块 str 数据,拥有所有权,支持增长和修改。
  • &str: 用一个胖指针借用这块 str 数据,只读,不拥有所有权。
            str(字符串数据的本体,DST)
           /                          \
    String                            &str
  拥有 str 数据                    借用 str 数据
  存在堆上,可增长修改          只读引用,64位平台是 16 字节

理解三者的关键在于: str 是数据本体,String&str 都是围绕它构建的

推荐先阅读 字符串延申 章节,深入理解 str&strString 的关系,再回来看这个章节会更容易。

字面量类型(&str)

由于 str 是 DST(动态大小类型),不能直接作为变量使用,所以通常以引用形式 &str 出现

定义语法: let var_name: &str = "string literal";

字符串字面量使用双引号包围,其类型被推导为 &str:

rust
let s = "hello";          // 推导为 &str
let s: &str = "hello";    // 等价写法,显式指定类型

字面量的存储原理

字符串字面量(&str)并不是先在堆上创建一个 String,再引用它来得到 &str 的。编译器对字符串字面量(&str)做了特殊处理:

  1. 编译期: 编译器将字符串字面量(&str)以硬编码的方式写入程序二进制文件中
  2. 加载期: 程序被加载时,字符串字面量(&str)被放入内存的全局字面量区(不在堆上,也不在栈上)
  3. 运行期: 执行到 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 转换(显式拷贝)
rust
let mut s = String::new();         // 新建可变空字符串
let s = String::from("hello");     // 从字面量构建
let s = "hello".to_string();       // 等价写法
let s = "hello".to_owned();        // 等价写法

String 堆拷贝原理

rust
let s = String::from("hello");

这行代码的执行过程: 编译器将 "hello" 硬编码写入二进制文件,程序加载时放入全局字面量区;运行到 String::from() 时,从字面量区将内容拷贝到堆内存;栈上的变量 s 保存指向堆内存的指针、长度和容量(共 3 个字长)。

 全局字面量区       堆内存            变量 s(栈上)
 ┌──────────┐     ┌──────────┐     ┌──────────────────────────┐
 │  "hello" │  →  │  "hello" │ ←── │  ptr + len(5) + cap(5)   │
 └──────────┘     └──────────┘     │         String            │
              (从字面量区拷贝而来)   └──────────────────────────┘

String 独有方法

String 独有一系列用于修改内容、管理内存和所有权转换的方法:

rust
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 隐式转换&strString 均可直接调用。

方法分类

查询

查询字符串属性或内容,返回相关信息,不修改原字符串,不转移所有权。

rust
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

迭代

借用原数据,迭代字符串内容,返回一个迭代器,每次迭代返回一个元素(如字符、字节、行等),元素类型取决于具体方法。

rust
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 切片,借用原数据,不修改原字符串。

rust
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,不修改原字符串,不转移原字符串所有权。

rust
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 切片,借用原数据,不修改原字符串,不转移原字符串所有权。

rust
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。

rust
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();

字符串索引与切片

字面量 &strString 不支持直接整数索引,只能通过切片访问,因为 Rust 的字符串是 UTF-8 编码,一个字符可能占 1 到 4 个字节,整数索引的语义不明确:

rust
let s = "hello";
let c = s[0];    // ❌ 编译错误: 不支持直接整数索引

使用切片时,必须在字符边界处切割,否则会 panic:

rust
let s = "Здравствуйте"; // 西里尔字母,每个字符 2 字节
let slice = &s[0..4];   // ✅ 正确: 4 字节 = 两个字符
println!("{}", slice);  // Зд

let slice = &s[0..3];   // ❌ 运行时 panic: 在字符中间切割

获取字符串长度

  • len() 返回字节数;
  • chars().count() 返回 Unicode 字符数(UTF-8 安全)。
rust
let s = "你好";
println!("{}", s.len());           // 6(每个汉字 3 字节)
println!("{}", s.chars().count()); // 2

含多字节字符时用 chars().count() 获取字符数

遍历字符串

  • chars(): 按 Unicode 字符迭代,返回 char 类型,推荐用于字符处理
  • bytes(): 按原始字节迭代,返回 u8 类型,用于字节级操作
rust
for c in "Зд".chars() {
    println!("{c}");
}
// 输出: З
//      д

for b in "Зд".bytes() {
    println!("{b}");
}
// 输出: 208  151  208  180

需要人能理解的字符时,使用 chars();需要机器处理的字节时,使用 bytes()

字符串拼接

+ 运算符

+ 底层调用 add() 方法,第一个参数接管 self 的所有权,必须是 String 类型,第二个参数为 &str:

标准库定义:

rust
// 简化后的概念结构
impl Add<&str> for String {
    type Output = String;
    fn add(self, rhs: &str) -> String { ... }
}

使用示例:

rust
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! 不获取任何所有权,适合多个字符串拼接:

rust
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!不转移任何参数稍低(额外分配)多段拼接、格式化

&strString 互转

&str → String

rust
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

rust
let owned = String::from("hello"); // String

// 1. 直接借用为 &str,不转移所有权
let borrowed: &str = &owned;
// 2. 使用 as_str 方法,等价于直接借用,但更显式地表示"获取 &str 切片"
let borrowed2: &str = owned.as_str();

基于 MIT 协议发布