Rust: Rust 字符串全面解析:&str 与 String 的奥秘

最后更新:2026-08-26

Rust 的字符串类型不是一种,而是两种——因为系统编程需要精确控制内存。

Rust 的字符串处理是很多初学者的困惑点:为什么有 String&str 两种?这和 C 语言的 char* 有什么不同?本课带你彻底搞懂。


1. 你将学到


2. 一位图书管理员的故事

(1) 痛苦:JSON 数据太长放不下

Maria 是一名图书管理员,她在开发一个小型图书检索系统来管理 5,000 本藏书

"如果有自动管理内存的字符串该多好……"她后来在 Rust 里找到了答案。

(2) Rust 的字符串方案

RUST
fn main() {
    // &str:字符串字面量,编译时就存在二进制里
    let title_book1: &str = "百年孤独";

    // String:堆分配的,可变长,自动管理内存
    let mut title_book2 = String::from("How to Make a Complete Map of an Infinite Universe");
    title_book2.push_str(" (Second Edition)");  // 追加内容

    println!("书 1: {} (长度: {} 字节)", title_book1, title_book1.len());
    println!("书 2: {} (长度: {} 字节)", title_book2, title_book2.len());
    // String 离开作用域时自动释放,不需要手动 free
}

Rust 的 String 像 Java 的字符串——自动管理内存;&str 像 C 的 const char*——只读引用。两者配合,兼顾性能和灵活性。


3. &str 和 String 的区别

(1) 内存模型

100%
graph LR
    subgraph "&str (字符串字面量)"
        S1_ptr[ptr ──→ ...rust...]
        S1_len[len: 4]
    end
    subgraph "String (堆分配)"
        S2_ptr[ptr ──→ Rust is...]
        S2_len[len: 10]
        S2_cap[cap: 16]
    end
    subgraph "存储位置"
        STACK[栈: &str<br>只读借用]
        HEAP[堆: String<br>拥有所有权]
    end
    S1_ptr -.->|编译时确定| STACK
    S2_ptr -.->|运行时分配| HEAP
特性 &str String
所有者 只读借用 拥有所有权
可变性 不可变 可追加/修改
存储位置 只读数据段(编译时) 堆(运行时分配)
创建方式 字面量 "hello" String::from().to_string()
适用场景 只读的字符串参数 需要修改或拥有字符串所有权

(2) String 常用方法速查

方法 返回类型 说明 示例
push_str(&str) () 追加字符串切片 s.push_str("abc")
push(char) () 追加单个字符 s.push('!')
len() usize 返回字节长度 s.len()
is_empty() bool 是否为空 s.is_empty()
trim() &str 去除两端空白 s.trim()
contains(&str) bool 是否包含子串 s.contains("rust")
replace(&str, &str) String 替换所有匹配 s.replace("a", "b")
split(char) Split 按分隔符分割 s.split(',')
to_lowercase() String 转小写 s.to_lowercase()
to_uppercase() String 转大写 s.to_uppercase()
chars() Chars 按字符遍历 s.chars()
bytes() Bytes 按字节遍历 s.bytes()
clear() () 清空字符串 s.clear()
capacity() usize 返回缓冲区容量 s.capacity()

(3) 创建方式

RUST
// &str 创建
let s1: &str = "hello, world";       // 字符串字面量
let s2: &str = r"raw string\n";      // 原始字符串(不转义)

// String 创建
let s3 = String::from("hello");
let s4 = "world".to_string();
let s5 = format!("{}-{}", s3, s4);   // 格式化创建
let s6: String = "hi".into();         // 通过类型转换

(4) 字符串转换方式速查

源类型 目标类型 转换方式 是否零开销
&str String "hello".to_string() 分配堆内存
&str String String::from("hello") 分配堆内存
&str String "hello".into() 分配堆内存
String &str &ss.as_str() 零开销(借用)
String &str &s[..] 零开销(切片)
&str &[u8] "hello".as_bytes() 零开销(借用)
String Vec<u8> s.into_bytes() 零开销(转移所有权)
Vec<u8> String String::from_utf8(v) 零开销(转移所有权,需验证 UTF-8)

4. 字符串常见操作

▶ 示例 1:String 的创建与修改(难度 ⭐)

RUST
// ============================================
// String 的创建、追加和拼接
// ============================================

fn main() {
    // 创建
    let mut s = String::from("Hello");

    // 追加
    s.push_str(", ");       // 追加字符串切片
    s.push('R');            // 追加单个字符
    s.push_str("ust!");
    println!("追加后: {}", s);

    // 拼接(+ 操作符会消费左侧 String)
    let s1 = String::from("Hello, ");
    let s2 = String::from("World!");
    let s3 = s1 + &s2;      // s1 被移动,不能再用了
    println!("拼接后: {}", s3);

    // format! 宏(不消费任何变量)
    let a = String::from("tic");
    let b = String::from("tac");
    let c = String::from("toe");
    let result = format!("{}-{}-{}", a, b, c);
    println!("format 结果: {}", result);
}

输出:

TEXT 📖 仅展示
追加后: Hello, Rust!
拼接后: Hello, World!
format 结果: tic-tac-toe

+ 操作符会消费(move)左侧的 String,所以 s1 在拼接后不能再使用。format! 不会消费任何变量,所有变量都保留所有权。


▶ 示例 2:字符串切片与索引(难度 ⭐⭐)

RUST
// ============================================
// 字符串切片——注意 UTF-8 边界!
// ============================================

fn main() {
    let s = String::from("Rust编程");

    // 获取字符串长度(字节数,不是字符数)
    println!("字节长度: {}", s.len());         // 10(4 字节 ASCII + 6 字节 UTF-8 中文)

    // 字符串切片(按字节索引,且必须在字符边界上)
    let slice1 = &s[0..4];                     // "Rust"(前 4 字节正好是一个完整字符边界)
    println!("前 4 字节切片: {}", slice1);

    // &s[0..5] 会 panic!(因为 4-5 字节是中文的第一个字节中间)

    // 按字符数遍历
    for c in s.chars() {
        print!("{} ", c);
    }
    println!();

    // 按字节遍历
    for b in s.bytes() {
        print!("{:02x} ", b);
    }
    println!();
}

输出:

TEXT 📖 仅展示
字节长度: 10
前 4 字节切片: Rust
R u s t 编 程
52 75 73 74 e7 bc 96 e7 a8 8b

Rust 字符串不支持按索引直接访问(如 s[0]),因为 UTF-8 的字符是变长的。必须用 .chars() 遍历字符或用字节切片。切片必须落在字符边界上,否则运行时崩溃。


▶ 示例 3:字符串常用方法(难度 ⭐⭐)

RUST
// ============================================
// 字符串常用的查询、替换、裁剪方法
// ============================================

fn main() {
    let s = "  Hello, Rust World!  ";

    // 去除两端空白
    println!("trim: '{}'", s.trim());

    // 包含判断
    println!("contains 'Rust': {}", s.contains("Rust"));

    // 替换
    let replaced = s.replace("Rust", "Go");
    println!("replace: '{}'", replaced);

    // 分割
    let parts: Vec<&str> = "apple,banana,cherry".split(',').collect();
    println!("split: {:?}", parts);

    // 大小写转换
    let lower = "HELLO".to_lowercase();
    let upper = "world".to_uppercase();
    println!("lower: {}, upper: {}", lower, upper);
}

输出:

TEXT 📖 仅展示
trim: 'Hello, Rust World!'
contains 'Rust': true
replace: '  Hello, Go World!  '
split: ["apple", "banana", "cherry"]
lower: hello, upper: WORLD

trim()split() 返回的是 &str,不创建新字符串。replace() 返回全新的 String。需要修改时考虑性能开销。


▶ 示例 4:UTF-8 字符串遍历与边界检查(难度 ⭐⭐⭐)

RUST
// ============================================
// UTF-8 字符串的字节、字符和图素簇
// ============================================

fn main() {
    let s = "Rust🦀你好";

    println!("字符串: {}", s);
    println!("字节长度: {}", s.len());
    println!("字符数量: {}", s.chars().count());

    println!("\n--- 逐字符遍历 ---");
    for (i, c) in s.chars().enumerate() {
        println!("字符 {}: '{}' (Unicode: U+{:04X})", i, c, c as u32);
    }

    println!("\n--- 逐字节遍历 ---");
    for (i, b) in s.bytes().enumerate() {
        print!("{:02x} ", b);
        if (i + 1) % 8 == 0 { println!(); }
    }
    println!();

    println!("\n--- 安全切片 ---");
    let ascii_part = &s[0..4];
    println!("ASCII 部分 &s[0..4]: '{}'", ascii_part);

    let emoji_start = 4;
    let emoji_end = emoji_start + 4;
    let emoji_part = &s[emoji_start..emoji_end];
    println!("Emoji 部分 &s[{}..{}]: '{}'", emoji_start, emoji_end, emoji_part);

    let chinese_start = emoji_end;
    let chinese_end = chinese_start + 6;
    let chinese_part = &s[chinese_start..chinese_end];
    println!("中文部分 &s[{}..{}]: '{}'", chinese_start, chinese_end, chinese_part);

    let combined = format!("{}{}{}", ascii_part, emoji_part, chinese_part);
    println!("拼接还原: {}", combined);
}

输出:

TEXT 📖 仅展示
字符串: Rust🦀你好
字节长度: 14
字符数量: 7

--- 逐字符遍历 ---
字符 0: 'R' (Unicode: U+0052)
字符 1: 'u' (Unicode: U+0075)
字符 2: 's' (Unicode: U+0073)
字符 3: 't' (Unicode: U+0074)
字符 4: '🦀' (Unicode: U+1F980)
字符 5: '你' (Unicode: U+4F60)
字符 6: '好' (Unicode: U+597D)

--- 逐字节遍历 ---
52 75 73 74 f0 9f a6 80
e4 bd a0 e5 a5 bd 

--- 安全切片 ---
ASCII 部分 &s[0..4]: 'Rust'
Emoji 部分 &s[4..8]: '🦀'
中文部分 &s[8..14]: '你好'
拼接还原: Rust🦀你好

UTF-8 中 ASCII 字符占 1 字节,中文占 3 字节,emoji 占 4 字节。切片必须落在字符边界上,否则运行时 panic。使用 .chars().enumerate() 可以安全地按字符处理。


▶ 示例 5:字符串解析与格式化实战(难度 ⭐⭐)

RUST
// ============================================
// 字符串解析、格式化和连接的综合运用
// ============================================

fn main() {
    let raw_data = "name=Alice;age=30;city=Beijing";

    println!("原始数据: {}", raw_data);
    println!("\n--- 解析键值对 ---");

    let pairs: Vec<&str> = raw_data.split(';').collect();
    for pair in pairs {
        let parts: Vec<&str> = pair.split('=').collect();
        if parts.len() == 2 {
            let key = parts[0].trim();
            let value = parts[1].trim();
            println!("{} => {}", key, value);
        }
    }

    println!("\n--- 构建新字符串 ---");
    let name = "Alice";
    let age = 30;
    let city = "Beijing";
    let summary = format!("{},{} 岁,来自 {}", name, age, city);
    println!("摘要: {}", summary);

    let mut report = String::from("人员报告\n");
    report.push_str(&format!("姓名: {}\n", name));
    report.push_str(&format!("年龄: {}\n", age));
    report.push_str(&format!("城市: {}\n", city));
    report.push_str("--- 结束 ---");
    println!("\n{}", report);

    let csv_line = ["Alice", "30", "Beijing"].join(",");
    println!("\nCSV 格式: {}", csv_line);
}

输出:

TEXT 📖 仅展示
原始数据: name=Alice;age=30;city=Beijing

--- 解析键值对 ---
name => Alice
age => 30
city => Beijing

--- 构建新字符串 ---
摘要: Alice,30 岁,来自 Beijing

人员报告
姓名: Alice
年龄: 30
城市: Beijing
--- 结束 ---

CSV 格式: Alice,30,Beijing

split() + collect() 是解析结构化文本的基本模式;format! 适合一次性拼接;push_str() 适合逐步构建长文本;join() 适合用分隔符连接数组。


❓ 常见问题

Q 为什么 Rust 要有两种字符串类型?
A 所有权和性能的平衡。
Q String 和 &str 怎么相互转换?
A &str → String 用 .to_string()String::from();String → &str 用 &ss.as_str()
Q 为什么 s[0] 不能取到第一个字符?
A 因为 UTF-8 是变长编码。
Q 字符串拼接用 + 还是 format!?
A 少量拼接用 +,多个片段用 format!。
Q 字符串可以修改吗?&str 和 String 谁能改?
A 只有 String 可以修改(mut 声明后可用 .push_str().push().insert() 等方法)。&str 是只读借用,不能修改指向的数据。

📖 小节


📝 作业

  1. 难度 ⭐:创建一个 String,依次调用 .push_str().push()+ 操作符和 format!,对比四种方式的结果。
  2. 难度 ⭐⭐:写一个函数 fn first_word(s: &str) -> &str,返回字符串的第一个单词(按空格分割)。分别用 &strString 调用它。
  3. 难度 ⭐⭐⭐:探索 UTF-8 边界问题——创建一个包含 emoji 🦀 的字符串 "Rust🦀",尝试对它做 &s[0..5]&s[0..6] 切片,观察哪个成功哪个崩溃。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏