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

513 阅读8分钟

背景

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

一、String和str的基本定义

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

1、String

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

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

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

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

2、str

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

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

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

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

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

    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

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

    let s = String::from("world");
    let slice: &str = s.as_str();
    

2、&str 转 String

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

    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、甚至别的切片。&str 是常规选择, 如果不想写&,对外的库推荐用 AsRef,String、&String、&str 都能直接传入。

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

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

      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"...")

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

      let raw_bytes: &[u8] = br"hello\nworld";
      let raw_bytes2: &[u8] = br#"with "quotes""#;
      
    • &str 是常规选择, 如果不想写&,对外的库推荐用 AsRef<str>,String、&String、&str 都能直接传入。

  • &mut str

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

      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

      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(这些容器需要拥有所有权)

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

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

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

      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(简单,灵活,最常见),结构体实例能够完全拥有字符串,常见于持久化数据模型(如数据库实体、配置对象)。业务开发首选,不用考虑生命周期。

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

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

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

    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_stringString
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_lowercaseASCII 转小写
make_ascii_uppercaseASCII 转大写
转移所有权into_boxed_str转为 Box<str>

3、只有str可用

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

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

五、总结

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

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

5bc79cb544aa45c1b48e02cf511d7241~tplv-73owjymdk6-jj-mark-v1_0_0_0_0_5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aSn6bG85LiD5oiQ6aWx_q75.webp