Skip to content

字符串类型

> 理解 &str(字符串切片)与 String(堆分配字符串)的本质区别及相互转换。

String 和 &str(重点难点)

这是 Rust 初学者最容易困惑的地方,我们详细讲解。

String vs &str 对比

┌─────────────────────────────────────────────────────────┐
│              String vs &str 对比                        │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  String                                               │
│  ├── 可增长的字符串                                   │
│  ├── 所有权在堆上                                     │
│  ├── 可以修改内容                                     │
│  ├── 大小:24 字节(指针 + 长度 + 容量)                │
│  └── 字面量创建:String::from("hello")                 │
│                                                         │
│  &str                                                 │
│  ├── 字符串切片/视图                                   │
│  ├── 借用数据,不拥有所有权                           │
│  ├── 通常不可变                                       │
│  ├── 大小:16 字节(指针 + 长度)                       │
│  └── 字面量创建:"hello"(类型是 &'static str)        │
│                                                         │
└─────────────────────────────────────────────────────────┘

内存布局可视化

字符串字面量 "hello"(类型:&'static str):

┌─────────────────────────────────────────────────┐
│          栈(Stack)                              │
│  ┌───────────────┬───────────────┐              │
│  │    指针       │     长度      │  ← &str     │
│  │  (指向堆)     │    (5)        │              │
│  └───────┬───────┴───────────────┘              │
│          │                                       │
│          ▼                                       │
│  ┌─────────────────────────────────────────┐    │
│  │          静态区(.rodata)                 │    │
│  │  "hello\0"                               │    │
│  │  ^ 字符串存储在这里                      │    │
│  └─────────────────────────────────────────┘    │
│                                                  │
│  特点:编译时确定,整个程序生命周期存在         │
└─────────────────────────────────────────────────┘


String(可增长字符串):

┌─────────────────────────────────────────────────┐
│          栈(Stack)                              │
│  ┌───────────────┬───────────────┬─────────┐   │
│  │    指针       │     长度      │  容量   │   │
│  │  (指向堆)     │    (5)        │  (10)   │   │
│  └───────┬───────┴───────────────┴─────────┘   │
│          │                                       │
│          ▼                                       │
│  ┌─────────────────────────────────────────┐    │
│  │          堆(Heap)                       │    │
│  │  "hello"  │  空闲空间     │              │    │
│  │  └──────┘ └─────────────┘               │    │
│  │   已用 (5)    可用 (5)                   │    │
│  └─────────────────────────────────────────┘    │
│                                                  │
│  特点:运行时分配,可以增长                      │
└─────────────────────────────────────────────────┘

基本用法

rust
fn main() {
    // &str - 字符串字面量(不可变)
    let s1: &str = "hello";

    // String - 可增长字符串
    let s2: String = String::from("hello");
    let s3: String = "hello".to_string();
    let s4: String = "hello".to_owned();

    println!("s1 (str): {}", s1);
    println!("s2 (String): {}", s2);
    println!("s3 (String): {}", s3);
    println!("s4 (String): {}", s4);

    // 类型验证
    println!("s1 类型:{:?}", std::any::type_name_of_val(&s1));
    println!("s2 类型:{:?}", std::any::type_name_of_val(&s2));
}
▶ Run

修改 String

rust
fn main() {
    let mut s = String::from("hello");

    // 追加字符串
    s.push_str(" world");
    println!("追加后:{}", s);

    // 追加单个字符
    s.push('!');
    println!("追加字符:{}", s);

    // 拼接
    let s1 = String::from("hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2;  // 注意:s1 被移动,s2 仍然可用
    println!("拼接:{}", s3);
    // println!("s1 = {}", s1);  // ❌ 错误,s1 已移动

    // 使用 format! 宏(不移动所有权)
    let s1 = String::from("hello");
    let s2 = String::from("world");
    let s3 = format!("{} {}", s1, s2);
    println!("format: {}", s3);
    println!("s1 仍可用:{}", s1);
    println!("s2 仍可用:{}", s2);
}
▶ Run

字符串切片(Deref coercion)

rust
fn main() {
    let mut s = String::from("hello world");

    // 获取 String 的切片
    let slice: &str = &s[0..5];  // "hello"
    println!("切片:{}", slice);

    // 自动解引用(Deref coercion)
    // &String 可以自动转换为 &str
    fn takes_str(s: &str) {
        println!("收到:{}", s);
    }

    let my_string = String::from("hello");
    takes_str(&my_string);  // &String → &str
    takes_str(my_string.as_str());  // 显式转换
    takes_str("literal");  // 字面量就是 &str
}
▶ Run

String 和&str 转换

rust
fn main() {
    // &str → String
    let s1: &str = "hello";
    let s2: String = s1.to_string();
    let s3: String = s1.to_owned();
    let s4: String = String::from(s1);

    // String → &str
    let string = String::from("hello");
    let str1: &str = &string;
    let str2: &str = string.as_str();

    // 函数参数
    fn print_string(s: &String) {
        println!("String: {}", s);
    }

    fn print_str(s: &str) {
        println!("str: {}", s);
    }

    let my_string = String::from("hello");

    print_string(&my_string);  // 需要 &String
    print_str(&my_string);     // &String 自动转为 &str
    print_str("literal");      // 字面量是 &str
    print_str(&my_string[0..3]); // 切片是 &str
}
▶ Run

常见陷阱

rust
fn main() {
    // 陷阱 1:试图修改&str
    let mut s: &str = "hello";
    // s.push_str(" world");  // ❌ &str 没有 push_str 方法

    // 正确做法:使用 String
    let mut s: String = String::from("hello");
    s.push_str(" world");
    println!("{}", s);

    // 陷阱 2:悬垂引用
    // fn get_string() -> &String {
    //     let s = String::from("hello");
    //     &s  // ❌ 错误:返回局部变量的引用
    // }

    // 正确做法:返回 String
    fn get_string() -> String {
        String::from("hello")
    }
    let owned = get_string();
    println!("{}", owned);

    // 陷阱 3:索引访问中文字符
    let s = String::from("你好");
    // let first = s[0];  // ❌ 错误!不能通过索引访问

    // 正确做法:使用字符迭代
    let first_char = s.chars().next().unwrap();
    println!("第一个字符:{}", first_char);

    // 字节长度 vs 字符长度
    println!("字节数:{}", s.len());  // 6 (每个中文 3 字节)
    println!("字符数:{}", s.chars().count());  // 2
}
▶ Run

实战:字符串处理

rust
fn main() {
    let text = String::from("  Hello, Rust!  ");

    // 去除空白
    let trimmed = text.trim();
    println!("去空白:'{}'", trimmed);

    // 大小写转换
    println!("大写:{}", trimmed.to_uppercase());
    println!("小写:{}", trimmed.to_lowercase());

    // 替换
    let replaced = trimmed.replace("Rust", "World");
    println!("替换:{}", replaced);

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

    // 查找
    if trimmed.contains("Rust") {
        println!("包含 Rust");
    }

    if trimmed.starts_with("Hello") {
        println!("以 Hello 开头");
    }

    if trimmed.ends_with("!") {
        println!("以!结尾");
    }

    // 查找位置
    if let Some(pos) = trimmed.find("Rust") {
        println!("Rust 在位置:{}", pos);
    }
}
▶ Run

小结

  • &str:字符串字面量/切片,存储在只读内存,不可变,零拷贝
  • String:堆分配的可增长字符串,可变
  • &strString.to_string()String::from()
  • String&str&ss.as_str()
  • Rust 字符串是 UTF-8 编码,不支持按字节索引

练习题

详见:练习题