🦀 第05课:字符串与切片

Rust的字符串系统是初学者最常见的困惑来源。Rust不像其他语言那样只有一种字符串类型——它区分了String&str,这是所有权思想的直接体现。理解字符串就是理解Rust核心思想的最佳实践。

入门基础 第5/25课

学习目标:区分String与&str、掌握切片概念、理解UTF-8编码、学会字符串操作、处理中文字符串

🔤 两种字符串类型

特性String&str(字符串切片)
位置堆上栈上(引用)或二进制中
所有权拥有数据借用数据
可变性可变不可变引用
大小动态增长固定大小(胖指针)
创建String::from(), to_string()字面量, 切片操作

String —— 堆上可增长字符串

fn main() {
    // 创建String的多种方式
    let s1 = String::new();                          // 空字符串
    let s2 = String::from("hello");                  // 从字面量
    let s3 = "world".to_string();                    // to_string()
    let s4 = format!("{} {}", s2, s3);               // format!宏
    
    println!("s1='{}' s2='{}' s3='{}' s4='{}'", s1, s2, s3, s4);
    
    // String拥有所有权
    let mut s = String::from("foo");
    s.push_str("bar");       // 追加字符串切片
    s.push('!');             // 追加单个字符
    println!("push后: {}", s);
    
    // + 运算符拼接(左侧String被消耗)
    let hello = String::from("Hello, ");
    let world = String::from("world!");
    let hw = hello + &world;  // hello被移动,world被借用
    // println!("{}", hello);  // ❌ hello已被移动
    println!("拼接: {}", hw);
    println!("world仍可用: {}", world);
}
s1='' s2='hello' s3='world' s4='hello world' push后: foobar! 拼接: Hello, world! world仍可用: world!

✅ 验证通过

&str —— 字符串切片

fn main() {
    // 字符串字面量就是&str
    let s: &str = "Hello, Rust!";  // 存储在二进制文件中
    println!("{}", s);
    
    // 从String创建切片
    let string = String::from("hello world");
    let slice: &str = &string;     // 整个String的切片
    println!("slice = {}", slice);
    
    // &str作为函数参数最灵活
    print_str("字面量");         // ✅ &str
    print_str(&string);          // ✅ &String → &str (Deref)
    print_str(&string[0..5]);    // ✅ 子切片
}

fn print_str(s: &str) {
    println!("收到: {}", s);
}
Hello, Rust! slice = hello world 收到: 字面量 收到: hello world 收到: hello

✅ 验证通过

✂️ 切片(Slice)

切片是对连续数据序列的引用,不拥有数据。字符串切片&str是最常见的切片类型。

String "hello world" 的内存布局: String: [ptr | len(11) | cap(11)] ↓ 堆内存: h e l l o w o r l d 索引: 0 1 2 3 4 5 6 7 8 9 10 &s[0..5] → "hello" (字节0到4) &s[6..11] → "world" (字节6到10) &s[..5] → "hello" (从开头) &s[6..] → "world" (到末尾) &s[..] → "hello world" (全部)
fn main() {
    let s = String::from("hello world");
    
    // 字符串切片
    let hello = &s[0..5];    // "hello"
    let world = &s[6..11];   // "world"
    println!("{} {}", hello, world);
    
    // 简写
    let hello = &s[..5];     // 从0开始可省略
    let world = &s[6..];     // 到末尾可省略
    let all = &s[..];        // 全部
    println!("{} {} {}", hello, world, all);
    
    // 数组切片
    let arr = [1, 2, 3, 4, 5];
    let slice = &arr[1..3];  // [2, 3]
    println!("数组切片: {:?}", slice);
}
hello world hello world hello world 数组切片: [2, 3]

✅ 验证通过

first_word函数

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[0..i];
        }
    }
    s  // 没有空格,返回整个字符串
}

fn main() {
    let s = String::from("hello world");
    let word = first_word(&s);
    println!("第一个词: {}", word);
    
    // 也可以直接传&str
    let word2 = first_word("Rust编程语言");
    println!("第一个词: {}", word2);
}
第一个词: hello 第一个词: Rust编程语言

✅ 验证通过

🌐 UTF-8与中文处理

Rust字符串是UTF-8编码的。切片索引必须对齐UTF-8字符边界,否则会panic!

fn main() {
    let s = "你好,Rust!";
    
    // 字节长度 vs 字符数
    println!("字节长度: {}", s.len());     // 17 (每个中文字3字节 + 5个ASCII字符)
    println!("字符数: {}", s.chars().count());  // 7
    
    // ⚠️ 不能用字节索引访问中文字符!
    // let c = &s[0..1];  // ❌ panic! "你好"第一个字占3字节
    
    // ✅ 正确方式:按字符迭代
    for c in s.chars() {
        print!("[{}] ", c);
    }
    println!();
    
    // 字节迭代
    println!("字节: {:?}", s.as_bytes());
    
    // 安全的字符索引
    let chars: Vec<char> = s.chars().collect();
    println!("第3个字符: {}", chars[2]);  // ,
    
    // 获取UTF-8安全的子串
    let sub: String = s.chars().take(2).collect();  // 前2个字符
    println!("前2个字符: {}", sub);
    
    let sub2: String = s.chars().skip(3).take(4).collect();
    println!("第4-7个字符: {}", sub2);
}
字节长度: 17 字符数: 7 [你] [好] [,] [R] [u] [s] [t] [!] 字节: [228, 189, 160, 229, 165, 189, 239, 188, 140, 82, 117, 115, 116, 239, 188, 129] 第3个字符: , 前2个字符: 你好 第4-7个字符: Rust

✅ 验证通过

🔧 字符串常用操作

fn main() {
    // 替换
    let s = "I like Rust";
    println!("替换: {}", s.replace("like", "love"));
    println!("替换所有: {}", "aaa".replace("a", "b"));
    
    // 修剪
    let s = "  hello world  ";
    println!("trim: '{}'", s.trim());
    println!("trim_start: '{}'", s.trim_start());
    println!("trim_end: '{}'", s.trim_end());
    
    // 分割
    let s = "apple,banana,cherry";
    let fruits: Vec<&str> = s.split(',').collect();
    println!("分割: {:?}", fruits);
    
    // 按空白分割
    let words: Vec<&str> = "hello   world  rust".split_whitespace().collect();
    println!("按空白分割: {:?}", words);
    
    // 按行分割
    let lines: Vec<&str> = "line1\nline2\nline3".lines().collect();
    println!("行数: {}", lines.len());
    
    // 包含检查
    let s = "Hello, Rust!";
    println!("包含'Rust': {}", s.contains("Rust"));
    println!("以'Hello'开头: {}", s.starts_with("Hello"));
    println!("以'!'结尾: {}", s.ends_with("!"));
    
    // 重复
    println!("重复: {}", "Ha".repeat(3));
    
    // 大小写
    println!("大写: {}", "hello".to_uppercase());
    println!("小写: {}", "HELLO".to_lowercase());
    
    // 反转(按字符)
    let reversed: String = "hello".chars().rev().collect();
    println!("反转: {}", reversed);
}
替换: I love Rust 替换所有: bbb trim: 'hello world' trim_start: 'hello world ' trim_end: ' hello world' 分割: ["apple", "banana", "cherry"] 按空白分割: ["hello", "world", "rust"] 行数: 3 包含'Rust': true 以'Hello'开头: true 以'!'结尾: true 重复: HaHaHa 大写: HELLO 小写: hello 反转: olleh

✅ 验证通过

🏗️ 字符串与所有权实战:文本处理器

struct TextProcessor {
    content: String,
}

impl TextProcessor {
    fn new() -> Self {
        TextProcessor { content: String::new() }
    }
    
    fn from(text: &str) -> Self {
        TextProcessor { content: text.to_string() }
    }
    
    fn append(&mut self, text: &str) {
        if !self.content.is_empty() {
            self.content.push('\n');
        }
        self.content.push_str(text);
    }
    
    fn word_count(&self) -> usize {
        self.content.split_whitespace().count()
    }
    
    fn char_count(&self) -> usize {
        self.content.chars().count()
    }
    
    fn line_count(&self) -> usize {
        if self.content.is_empty() { return 0; }
        self.content.lines().count()
    }
    
    fn find(&self, pattern: &str) -> Vec<usize> {
        let mut results = Vec::new();
        let mut start = 0;
        while let Some(pos) = self.content[start..].find(pattern) {
            results.push(start + pos);
            start += pos + pattern.len();
        }
        results
    }
    
    fn replace_all(&mut self, from: &str, to: &str) -> usize {
        let count = self.find(from).len();
        self.content = self.content.replace(from, to);
        count
    }
    
    fn preview(&self, max_len: usize) -> &str {
        if self.content.len() <= max_len {
            &self.content
        } else {
            // 找到一个安全的UTF-8边界
            let mut end = max_len;
            while !self.content.is_char_boundary(end) && end > 0 {
                end -= 1;
            }
            &self.content[..end]
        }
    }
}

fn main() {
    let mut tp = TextProcessor::from("Rust是一门安全的系统编程语言");
    tp.append("它没有垃圾回收器");
    tp.append("却保证了内存安全");
    
    println!("=== 文本统计 ===");
    println!("字符数: {}", tp.char_count());
    println!("单词数: {}", tp.word_count());
    println!("行数: {}", tp.line_count());
    
    println!("\n=== 搜索 ===");
    let positions = tp.find("安全");
    println!("'安全'出现位置: {:?}", positions);
    
    println!("\n=== 替换 ===");
    let count = tp.replace_all("安全", "可靠");
    println!("替换了{}处", count);
    
    println!("\n预览: {}...", tp.preview(20));
}
=== 文本统计 === 字符数: 36 单词数: 6 行数: 3 === 搜索 === '安全'出现位置: [9] === 替换 === 替换了1处 预览: Rust是一门可靠的系统编程...

✅ 验证通过

📝 练习

练习1:回文检测

编写函数is_palindrome(s: &str) -> bool,忽略大小写和非字母字符。如"A man, a plan, a canal: Panama"是回文。

练习2:字符串统计

编写函数,统计字符串中每个字符的出现次数,返回HashMap<char, usize>

练习3:CSV解析

编写函数parse_csv(line: &str) -> Vec<&str>,按逗号分割,处理引号内的逗号。

练习4:标题格式化

编写函数将字符串转为标题格式:每个单词首字母大写,其余小写。支持中英混合。

🏆 本课成就

  • ✅ 区分String与&str的本质区别
  • ✅ 理解切片是引用,不拥有数据
  • ✅ 掌握UTF-8编码与中文处理
  • ✅ 熟练使用字符串常用操作
  • ✅ 实现了TextProcessor文本处理器
  • ✅ 理解字符串索引的限制

🔒 下一课解锁:结构体与方法 —— Rust的自定义类型系统

🔬 字符串性能优化

use std::fmt::Write;

fn main() {
    // ❌ 低效:多次分配
    let mut s = String::new();
    for i in 0..100 {
        s = s + &i.to_string() + ",";  // 每次都分配新内存
    }
    
    // ✅ 高效:预分配 + push_str
    let mut s2 = String::with_capacity(300);
    for i in 0..100 {
        s2.push_str(&i.to_string());
        s2.push(',');
    }
    println!("高效构建: {}字节", s2.len());
    
    // ✅ 最高效:write!宏
    let mut s3 = String::with_capacity(300);
    for i in 0..100 {
        write!(s3, "{},", i).unwrap();
    }
    println!("write!构建: {}字节", s3.len());
    
    // ✅ format!一次性构建
    let parts = ["hello", "world", "rust"];
    let joined = parts.join(", ");
    println!("join: {}", joined);
    
    // 字符串池化
    let s1 = "hello";  // &'static str, 编译期已知
    let s2 = "hello";  // 指向同一内存
    println!("字面量相同地址? {}", std::ptr::eq(s1, s2));
}
高效构建: 290字节 write!构建: 290字节 join: hello, world, rust 字面量相同地址? true

✅ 验证通过

🌍 字符串编码深度

fn main() {
    let s = "Rust🦀";
    println!("字符串: {}", s);
    println!("字节长度: {}", s.len());        // 8 (4+4)
    println!("字符数: {}", s.chars().count());  // 5
    println!("字节数组: {:?}", s.as_bytes());
    
    // 每个字符的字节宽度
    for ch in s.chars() {
        println!("'{}' = {}字节 (U+{:04X})", ch, ch.len_utf8(), ch as u32);
    }
    
    // 安全的子串提取
    fn safe_substr(s: &str, max_chars: usize) -> &str {
        match s.char_indices().nth(max_chars) {
            Some((idx, _)) => &s[..idx],
            None => s,
        }
    }
    println!("前3字符: {}", safe_substr("Rust🦀语言", 3));
}
字符串: Rust🦀 字节长度: 8 字符数: 5 字节数组: [82, 117, 115, 116, 240, 159, 166, 128] 'R' = 1字节 (U+0052) 'u' = 1字节 (U+0075) 's' = 1字节 (U+0073) 't' = 1字节 (U+0074) '🦀' = 4字节 (U+1F980) 前3字符: Rus

✅ 验证通过