Rust 新手必看:彻底搞懂 String 和 &str,不再被坑!

背景

这是Rust九九八十难的第五篇。关于字符串这块,平常在用的时候,有没有感觉有点不一样。其他语言如java,js等,字符串就是字符串String,偶尔用下char,很好区分。在rust里,字符串分为 String 类型和 str 类型 ,有时候传 &String 类型 或者 &str ,用起来有点儿乱,有时传哪个都能跑,有时仅一个能用,让人火大。今天整理下 Stringstr 的区别,最后附个速查表,方便快速使用。

一、String和str的基本定义

熟悉的同学可以直接跳过,这块做个简单说明。

1、String

  • String : 是封装了Vec的结构体。在堆上分配内存,支持修改扩容。

    rust 复制代码
    栈: [String { ptr, len, cap }]
           |
           v
    堆: [ h e l l o ]
  • &String : 加上&就是对 String 结构体的不可变借用(&符号不了解的可以看下这篇: 还在分不清 Rust 的 & 怎么用?10 分钟带你彻底搞懂

    rust 复制代码
    栈: [String { ptr, len, cap }]
    栈: [&String] -----> String 结构体
                           |
                           v
                         堆数据
  • &mut String: 继续加mut就是可变借用,可以修改字符串:push、pop、clear、resize 等。

    rust 复制代码
    栈: [String { ptr, len, cap }]
    栈: [&mut String] --> String 结构体(可修改)

2、str

  • str : 不能直接用,也很少用(比如借助Box,Box使用可以看这篇)。因为Rust 编译器在编译期需要知道一个类型的 大小 (size) ,才能在栈上分配它。官方文档定义 str 是DST (Dynamically Sized Type),编译时不知道大小。它本质上是 一段 UTF-8 字节序列,大小只有在运行时才知道。所以编译器没法说 "str 类型占多少字节"。要用必须放在引用或智能指针里。

  • &str :是对字符串内容的不可变借用。有三个来源:String、字符串字面量、甚至某个切片。如下面的示意图:

    rust 复制代码
    &String 在栈: { ptr -> 堆数据, len, cap }
    &str   在栈: { ptr -> 堆数据, len }
    堆: [ h e l l o ]

    ​ 可以对比的看,&String借用的是整个 String 对象&str借用的是堆中的字符串数据片段

  • &mut str :对某段字符串内容的可变借用。很少单独使用,多见于切片操作:

    rust 复制代码
    let mut s = String::from("hello");
    let slice: &mut str = &mut s[..]; // 可变切片

3、官方链接

二、String与str的相互转换

上面说到,&String是对 String 的不可变借用,而&str可以看成是字符串的通用视图,不管底层是 String 还是字面量 "hello"。在Rust 里 &String&str 的转换非常自然:

1、&String 转 &str

  • 自动解引用(Deref coercion ) 在大多数函数调用或赋值场景下,&String 会自动转换成 &str

    rust 复制代码
    fn print_str(s: &str) {
        println!("{}", s);
    }
    
    fn main() {
        let s = String::from("hello");
        print_str(&s);   // &String 自动转成 &str
    }
  • 手动调用.as_str()

    rust 复制代码
    let s = String::from("world");
    let slice: &str = s.as_str();

2、&str 转 String

  • 克隆数据(堆分配新的 String),有了新的所有权:

    rust 复制代码
    let s: &str = "hello";
    let string: String = s.to_string();// 新的 String,堆上存放 "hello"
    let string2: String = String::from(s);
  • 如果只是想借用,不需要转成 String,直接用 &str 就行。

三、使用场景

1、str常用场景

  • &str

    • 更加通用和高效,大多数函数参数都应该写成 &str。它能同时接受字面量、String、甚至别的切片。

      rust 复制代码
      fn greet(name: &str) {
          println!("Hello, {}!", name);
      }
      
      fn main() {
          greet("Alice");              // 字面量
          greet(&String::from("Bob")); // &String
      }
    • 定义字符串字面量(放在了只读数据段),零开销,无需堆分配,自动安全管理。适合固定文本、常量配置、消息模板

      rust 复制代码
      static GREETING: &str = "Hello, world!";
      
      fn main() {
          println!("{}", GREETING);
      }
    • 定义原始字面量(r"...")

      rust 复制代码
      let raw: &str = r"hello\nworld"; 
      // 内容是: h e l l o \ n w o r l d
      
      //不处理转义字符,可以用多个 # 来包裹,避免和内部引号冲突:
      let raw: &str = r#"a string with "quotes""#;
      let raw2: &str = r##"raw with "# inside"##;
    • 定义字节字符串字面量(b"...")

      rust 复制代码
      let bytes: &[u8; 5] = b"hello";
    • 原始字节字符串(br"..."br#"..."#

      rust 复制代码
      let raw_bytes: &[u8] = br"hello\nworld";
      let raw_bytes2: &[u8] = br#"with "quotes""#;
  • &mut str

    • 就地修改内容长度不变,用&mut str 更优雅,比如下面的例子:

      rust 复制代码
      fn shout(buf: &mut str) {
          buf.make_ascii_uppercase();
      }
      
      fn main() {
          let mut s = String::from("hello rust");
          shout(&mut s);
          println!("{}", s); // "HELLO RUST"
      }
    • 部分切片修改,只能用&mut str

      rust 复制代码
      fn fix_prefix(buf: &mut str) {
          buf.make_ascii_uppercase();
      }
      
      fn main() {
          let mut s = String::from("hello_world");
          {
              let prefix = &mut s[0..5]; // "hello"
              fix_prefix(prefix);
          }
          println!("{}", s); // "HELLO_world"
      }

      只有 &mut str 能直接操作子串;&mut String 操作的是整个字符串。

2、String常用场景

  • a、非引用方式。

    当需要 可变、拥有所有权 的字符串时,用 String。比如函数返回值

    需要转移所有权常见场景:

    • 需要拼接、修改字符串

    • 需要把字符串存入 VecHashMap(这些容器需要拥有所有权)

      rust 复制代码
      let mut s = String::from("hi");
      s.push_str(", Rust!");  // ✅ 可修改
  • b、&String

    • 一般不直接写函数参数为 &String,因为这会限制调用者必须传入 String,而不能传入字面量。
    • 所以 函数签名几乎总是用 &str ,除非你真的需要对 String 的特殊操作(很少见)。
  • c、 &mut String

    • 修改原有的String,传参必须用 &mut String。典型场景:回溯、拼接、扩展、删除原来字符

      rust 复制代码
      fn build_sentence(buf: &mut String) {
          buf.push('H');
          buf.push_str("ello");
          buf.push(' ');
          buf.push_str("Rust");
      }
      
      fn shrink(buf: &mut String) {
          buf.push_str("abcde");
          buf.pop();      // 删除 'e'
          buf.truncate(3); // 删除到 "abc"
      }
      
      
      fn main() {
          let mut s = String::new();
          build_sentence(&mut s);
          println!("{}", s); // "Hello Rust"
          
          let mut s1 = String::new();
          shrink(&mut s1);
          println!("{}", s1); // "abc"
      }

3、结构体的字符串怎么选

  • 大多数情况 :用 String(简单,灵活,最常见),结构体实例能够完全拥有字符串,常见于持久化数据模型(如数据库实体、配置对象)。业务开发首选,不用考虑生命周期。

    rust 复制代码
    struct User {
        name: String,
        email: String,
    }
  • 只读数据,依赖外部生命周期 :用 &str。如下面例子,必须加生命周期 'a,告诉编译器:User 的存活时间 ≤ 借用数据的存活时间。其他如果是全局字面量可以用'static。频率上不常用。

    rust 复制代码
    struct User<'a> {
        name: &'a str,
        email: &'a str,
    }
  • 要兼顾引用和所有权 :用 Cow<'a, str>。结构体既能接受引用的字符串,也能存拥有的字符串。如解析配置、文本处理函数的参数。做一些序列化,解析工具库的开发出场较多。

    rust 复制代码
    use std::borrow::Cow;
    
    struct User<'a> {
        name: Cow<'a, str>,
    }
  • 需要优化内存布局 :用 Box<str>。比和 String内存更紧凑,存储后不能再修改的字符串,占用更小,内存布局更稳定(适合 FFI 或内存优化场景)。嵌入式可能碰到,业务开发不建议用。

    rust 复制代码
    struct User {
        name: Box<str>,
    }

四、&str API 与 String 兼容对照表

平常开发混着用,两者定义的方法很像,但也有区别,为了方便快速查阅,这里整理一份api对照表。

1、相同api(String 自动解引用为 str,所以这些方法通用)

分类 方法 作用
基本信息 len 返回字节长度
is_empty 是否为空
chars 返回字符迭代器
bytes 返回字节迭代器
char_indices 字符+索引迭代
as_ptr 获取原始指针
as_bytes 获取字节切片 &[u8]
子串检查 contains 是否包含子串
starts_with 是否有前缀
ends_with 是否有后缀
find 查找子串位置
rfind 从右侧查找
matches 查找所有匹配
match_indices 查找所有匹配(带索引)
子串获取 get 安全切片
[range] 下标切片
split_at 按下标拆分
分割与迭代 split 按分隔符切分
rsplit 从右边切分
split_whitespace 按空白切分
lines 按行切分
修剪 trim 去掉首尾空白
trim_start 去掉开头空白
trim_end 去掉结尾空白
trim_matches 去掉匹配模式
转换 to_lowercase 转小写 (返回 String)
to_uppercase 转大写 (返回 String)
to_string String
repeat 重复拼接 (返回 String)
比较 eq 判断相等
cmp 比较顺序

2、只有String可用

类别 方法 作用
容量管理 capacity 当前分配的容量
reserve 预留额外容量
reserve_exact 精确预留容量
shrink_to_fit 释放多余容量
修改 push 追加一个字符
push_str 追加一个字符串
insert 在指定位置插入字符
insert_str 插入字符串
remove 删除某个字符
retain 按条件保留字符
truncate 截断到指定长度
clear 清空字符串
make_ascii_lowercase ASCII 转小写
make_ascii_uppercase ASCII 转大写
转移所有权 into_boxed_str 转为 Box<str>

3、只有str可用

这些方法是 切片级别 的,不会改变底层数据,通常返回新的切片或迭代器。

类别 方法 作用
模式匹配 strip_prefix 去掉前缀,返回子串
strip_suffix 去掉后缀,返回子串
切片工具 split_once 按第一次匹配切分
rsplit_once 按最后一次匹配切分
指针工具 as_ptr_range 得到指针范围
转义工具 escape_debug 可打印调试转义
escape_default 默认转义
escape_unicode Unicode 转义

五、总结

字符串几乎是所有语言使用最广泛的结构,而且尽量做了封装,但Rust这里用起来并不舒服,它拆分String和Str。Stackoverflow上有讨论,可能跟它的设计哲学和所有权有关系。其他二和三总结了关系和场景等,熟悉的可以跳到第四节,用这个速查表,快速定位问题。

本人公众号大鱼七成饱,所有历史文章都会在上面同步。

相关推荐
Victor35616 分钟前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack16 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo17 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor35618 分钟前
MongoDB(3)什么是文档(Document)?
后端
牛奔2 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌7 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
KYGALYX9 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
爬山算法9 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端