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上有讨论,可能跟它的设计哲学和所有权有关系。其他二和三总结了关系和场景等,熟悉的可以跳到第四节,用这个速查表,快速定位问题。

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

相关推荐
野犬寒鸦3 小时前
从零起步学习Redis || 第五章:利用Redis构造分布式全局唯一ID
java·服务器·数据库·redis·分布式·后端·缓存
做运维的阿瑞5 小时前
Python原生数据结构深度解析:从入门到精通
开发语言·数据结构·后端·python·系统架构
璨sou5 小时前
Rust语言--基础入门到应用
后端·rust
一只学java的小汉堡5 小时前
Spring Boot 配置详解:从引导器到注解实战(初学者指南)
java·spring boot·后端
__XYZ5 小时前
Vala编程语言高级特性-弱引用和所有权
c语言·开发语言·后端·c#
IT_陈寒5 小时前
Python开发者必坑指南:3个看似聪明实则致命的‘优化’让我损失了50%性能
前端·人工智能·后端
Ivanqhz6 小时前
Rust的错误处理
开发语言·后端·rust
java1234_小锋8 小时前
[免费]基于Python的Flask+Vue进销存仓库管理系统【论文+源码+SQL脚本】
后端·python·flask
fly-phantomWing11 小时前
Maven的安装与配置的详细步骤
java·后端·maven·intellij-idea