【问题标题】:What are the differences between Rust's `String` and `str`?Rust 的 `String` 和 `str` 有什么区别?
【发布时间】:2022-01-23 03:34:12
【问题描述】:

为什么 Rust 有 StringstrStringstr 有什么区别?什么时候使用String 而不是str,反之亦然?其中一个会被弃用吗?

【问题讨论】:

    标签: string rust


    【解决方案1】:

    String是动态堆字符串类型,如Vec:当你需要拥有或修改你的字符串数据时使用它。

    str 是内存中某处动态长度的 UTF-8 字节的不可变1 序列。由于大小未知,因此只能在指针后面处理。这意味着str 通常2 显示为&str:对一些 UTF-8 数据的引用,通常称为“字符串切片”或仅称为“切片”。 A slice 只是一些数据的视图,这些数据可以在任何地方,例如

    • 在静态存储中:字符串文字 "foo"&'static str。数据被硬编码到可执行文件中,并在程序运行时加载到内存中。

    • 在分配给String的堆内String的数据的String dereferences to a &str view

    • 在堆栈上:例如下面创建一个堆栈分配的字节数组,然后得到一个view of that data as a &str

      use std::str;
      
      let x: &[u8] = &[b'a', b'b', b'c'];
      let stack_str: &str = str::from_utf8(x).unwrap();
      

    总之,如果您需要拥有的字符串数据(例如将字符串传递给其他线程,或在运行时构建它们),请使用String,如果您只需要查看字符串的视图,请使用&str

    这与向量Vec<T> 和切片&[T] 之间的关系相同,并且类似于一般类型的按值T 和按引用&T 之间的关系。


    1str 是固定长度的;您不能写入超出结尾的字节,或留下尾随无效字节。由于 UTF-8 是一种可变宽度编码,因此在许多情况下,这有效地强制所有 strs 是不可变的。一般来说,突变需要比以前写入更多或更少的字节(例如,用ä(2+字节)替换a(1字节)将需要在str中腾出更多空间)。有一些特定的方法可以就地修改&mut str,主要是那些只处理ASCII字符的方法,比如make_ascii_uppercase

    2Dynamically sized types 允许 Rc<str> 自 Rust 1.2 以来的引用计数 UTF-8 字节序列。 Rust 1.21 允许轻松创建这些类型。

    【讨论】:

    • “UTF-8 字节序列(未知长度)” - 这是过时的吗? docs 表示“&str 由两个部分组成:指向某些字节的指针和长度。”
    • 它并没有过时(表示相当稳定),只是有点不精确:它不是静态已知的,不像 [u8; N]
    • @mrec 它在编译时是未知的,不能假设它的大小,例如,在创建堆栈帧时。因此为什么它经常被视为一个引用,这个引用在编译时是一个已知的大小,也就是一个指针的大小。
    • @cjohansson 静态分配的对象通常既不存储在堆上,也不存储在堆栈上,而是存储在它们自己的内存区域中。
    • @lxx,不,Rust 的所有权和借用开始了:编译器不会让你持有指向超出范围并被释放的 &str 切片。在垃圾收集语言中,切片可以在主所有者消失后存在,但在 Rust 中则不能:编译器强制程序员明确选择如何处理它,例如不要共享内存(通过使用.to_owned() 来创建一个单独的String),或者像你说的那样共享内存(使用kimundi.github.io/owning-ref-rs/owning_ref/… 之类的东西)。
    【解决方案2】:

    我有 C++ 背景,我发现用 C++ 术语思考 String&str 非常有用:

    • Rust String 就像 std::string;它拥有内存并执行管理内存的繁琐工作。
    • Rust &str 类似于char*(但更复杂一点);它指向一个块的开头,就像你可以得到一个指向 std::string 内容的指针一样。

    它们中的任何一个会消失吗?我不这么认为。它们有两个目的:

    String 保留缓冲区,使用起来非常实用。 &str 是轻量级的,应该用于“查看”字符串。您可以搜索、拆分、解析甚至替换块,而无需分配新内存。

    &str 可以查看String 的内部,因为它可以指向一些字符串文字。以下代码需要将文字字符串复制到String 托管内存中:

    let a: String = "hello rust".into();
    

    以下代码允许您使用文字本身而无需复制(但只读)

    let a: &str = "hello rust";
    

    【讨论】:

    • 像 string_view?
    • 是的,类似于 string_view,但对语言来说是固有的,并且适当地借用检查。
    【解决方案3】:

    str,只用作&str,是一个字符串切片,一个UTF-8字节数组的引用。

    String 以前是 ~str,一个可增长的、拥有的 UTF-8 字节数组。

    【讨论】:

    • 从技术上讲,以前的 ~str 现在是 Box<str>
    • @jv110:不,因为~str 是可增长的,而Box<str> 是不可增长的。 (~str~[T] 可以神奇地增长,与任何其他 ~-object 不同,这正是引入 StringVec<T> 的原因,因此规则都简单明了且一致。)
    【解决方案4】:

    它们实际上完全不同。首先,str 只不过是类型级别的东西;它只能在类型级别进行推理,因为它是所谓的动态大小类型(DST)。 str 占用的大小在编译时无法知道,它取决于运行时信息——它不能存储在变量中,因为编译器需要在编译时知道每个变量的大小。 str 在概念上只是一行 u8 字节,保证它形成有效的 UTF-8。行有多大?直到运行时才有人知道,因此它不能存储在变量中。

    有趣的是&str 或任何其他指向str 的指针,如Box<str> 确实 在运行时存在。这就是所谓的“胖指针”;它是一个带有额外信息的指针(在这种情况下是它指向的东西的大小),所以它是原来的两倍。事实上,&str 非常接近 String(但不接近 &String)。 &str 是两个字;一个指向str 的第一个字节的指针和另一个描述str 长多少字节的数字。

    与所说的相反,str 不需要是不可变的。如果您可以将&mut str 作为指向str 的独占指针,则可以对其进行变异,并且对其进行变异的所有安全函数都可以保证支持UTF-8 约束,因为如果违反了该约束,那么我们将具有未定义的行为库假定此约束为真并且不检查它。

    那么String 是什么?那是三个字;两个与&str 相同,但它添加了第三个单词,即堆上str 缓冲区的容量,始终在堆上(str 不一定在堆上)它在填充之前管理并且不得不重新分配。正如他们所说,String 基本上拥有 str;它控制它并可以调整它的大小并在它认为合适时重新分配它。所以Stringstr 更接近&str

    另一件事是Box<str>;这也拥有str,它的运行时表示与&str相同,但它也拥有str,不像&str,但它不能调整它的大小,因为它不知道它的容量,所以基本上是Box<str>可以看作是固定长度的String,不能调整大小(如果要调整大小,可以随时将其转换为String)。

    [T]Vec<T> 之间存在非常相似的关系,除了没有 UTF-8 约束并且它可以容纳任何大小不是动态的类型。

    str在类型层面的使用主要是用&str创建泛型抽象;它存在于类型级别以便能够方便地编写特征。理论上 str 作为类型的东西不需要存在,只有 &str 但这意味着必须编写许多额外的代码,现在可以是通用的。

    &str 非常有用,因为它能够拥有 String 的多个不同子字符串而无需复制;正如String 拥有在它管理的堆上的str 所说,如果你只能用一个新的String 创建一个String 的子字符串,它就必须复制,因为一切都在Rust 只能有一个所有者来处理内存安全问题。因此,例如,您可以对字符串进行切片:

    let string: String   = "a string".to_string();
    let substring1: &str = &string[1..3];
    let substring2: &str = &string[2..4];
    

    我们有同一个字符串的两个不同的子字符串strs。 string 是拥有堆上实际完整的str 缓冲区的那个,&str 子字符串只是指向堆上该缓冲区的胖指针。

    【讨论】:

    • "它不能存储在变量中,因为编译器需要在编译时知道每个变量的大小是多少" > 你能解释一下为什么编译器不能生成使用 run 的二进制代码吗-关于字符串长度的时间信息,好吗?这是一种 Rust 设计约束吗?
    • @Mergasov 可以,但这在性能方面会非常低效,并且会完全改变依赖于已知信息的函数调用约定。函数调用堆栈的大小和每个变量的位置在编译时是已知的,这对于生成高效的程序非常重要,这也是堆栈比堆快几个数量级的原因。在这种情况下,将它简单地放在指针后面的堆上要容易得多。它本质上是将堆栈变成第二个堆。
    【解决方案5】:

    类似于String的是str,而不是它的切片,也称为&str

    str 是一个字符串文字,基本上是一个预先分配的文本:

    "Hello World"
    

    此文本必须存储在某个地方,因此它与程序的机器代码一起存储在可执行文件的数据部分中,作为字节序列 ([u8])。因为文本可以是任意长度,它们是动态大小的,它们的大小只有在运行时才知道:

    +----+-----+-----+-----+-----+----+----+-----+-----+-----+-----+
    |  H |  e  |  l  |  l  |  o  |    |  W |  o  |  r  |  l  |  d  |
    +----+-----+-----+-----+-----+----+----+-----+-----+-----+-----+
    
    +----+-----+-----+-----+-----+----+----+-----+-----+-----+-----+
    | 72 | 101 | 108 | 108 | 111 | 32 | 87 | 111 | 114 | 108 | 100 |
    +----+-----+-----+-----+-----+----+----+-----+-----+-----+-----+
    

    我们需要一种访问存储文本的方法,这就是切片的用武之地。

    slice,[T] 是内存块的视图。无论是否可变,切片总是借用,这就是为什么它总是在 pointer& 后面。

    让我们解释动态调整大小的含义。一些编程语言,如 C,在其字符串的末尾附加一个零字节 (\0) 并记录起始地址。要确定字符串的长度,程序必须从起始位置遍历原始字节,直到找到这个零字节。因此,文本的长度可以是任意大小,因此它是动态大小的。

    C 程序必须遍历文本才能找到字符串的大小。然而,Rust 采用了与寻找 \0 不同的方法。它使用切片。切片存储str 开始的地址以及它需要多少字节。该切片也存储在二进制文件中。这比附加零字节要好,因为在编译过程中会提前进行计算。

    因此,“Hello World”表达式返回一个胖指针,其中包含实际数据的地址及其长度。这个指针将是我们对实际数据的句柄,这个句柄也将存储在我们的程序中。现在数据在指针后面,编译器在编译时就知道它的大小。

    由于文本存储在源代码中,它将在运行程序的整个生命周期内有效,因此将具有static 生命周期。

    所以,“Hello Word”表达式的返回值应该反映这两个特征,它确实:

    let s: &'static str = "Hello World";
    

    你可能会问为什么它的类型写成str而不是[u8],这是因为数据总是保证是一个有效的UTF-8序列。并非所有 UTF-8 字符都是单字节,有些需要 4 个字节。所以 [u8] 是不准确的。

    如果你反汇编一个已编译的 Rust 程序并检查可执行文件,你会看到多个 strs 彼此相邻存储在数据部分中,没有任何指示一个开始和另一个结束的位置。

    编译器更进一步。如果在程序中的多个位置使用相同的静态文本,Rust 编译器将优化您的程序并在可执行文件的数据部分中创建一个二进制块,并且代码中的每个切片都指向这个二进制块。

    例如,编译器为以下代码创建一个内容为“Hello World”的连续二进制文件,即使我们使用三个不同的文字和"Hello World"

    let x: &'static str = "Hello World";
    let y: &'static str = "Hello World";
    let z: &'static str = "Hello World";
    

    另一方面,String 是一种特殊类型,将其值存储为 u8 的向量。以下是String 类型在源代码中的定义方式:

    pub struct String {
        vec: Vec<u8>,
    }
    

    作为向量意味着它像任何其他向量值一样被堆分配和调整大小。

    专业化意味着它不允许任意访问并强制执行某些检查以确保数据始终是有效的 UTF-8。除此之外,它只是一个向量。

    所以 a String 是一个可调整大小的缓冲区,用于保存 UTF-8 文本。这个缓冲区是在堆上分配的,所以它可以根据需要或请求增长。我们可以以任何我们认为合适的方式填充这个缓冲区。我们可以改变它的内容。

    如果你仔细看,vec 字段保持私有以强制执行有效性。由于它是私有的,我们不能直接创建 String 实例。之所以保持私有,是因为并非所有字节流都会产生有效的 utf-8 字符,并且与底层字节的直接交互可能会损坏字符串。我们通过方法创建u8 字节并且方法运行某些检查。我们可以说,私有化并通过方法进行受控交互提供了一定的保证。

    在String类型上定义了几种方法来创建String实例,new就是其中之一:

    pub const fn new() -> String {
      String { vec: Vec::new() }
    }
    

    我们可以用它来创建一个有效的字符串。

    let s = String::new();
    println("{}", s);
    

    不幸的是它不接受输入参数。因此结果将是有效的,但是是一个空字符串,但是当容量不足以容纳分配的值时,它会像任何其他向量一样增长。但应用程序性能会受到影响,因为增长需要重新分配。

    我们可以用不同来源的初始值填充底层向量:

    来自字符串文字

    let a = "Hello World";
    let s = String::from(a);
    

    请注意,str 仍会创建,其内容会通过String.from 复制到堆分配的向量中。如果我们检查可执行二进制文件,我们将在数据部分中看到内容为“Hello World”的行字节。这是一些人错过的非常重要的细节。

    来自原始零件

    let ptr = s.as_mut_ptr();
    let len = s.len();
    let capacity = s.capacity();
    
    let s = String::from_raw_parts(ptr, len, capacity);
    

    来自一个角色

    let ch = 'c';
    let s = ch.to_string();
    

    来自字节向量

    let hello_world = vec![72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100];
    // We know it is valid sequence, so we can use unwrap
    let hello_world = String::from_utf8(hello_world).unwrap();
    println!("{}", hello_world); // Hello World
    

    这里我们有另一个重要的细节。向量可能有任何值,不能保证它的内容是有效的 UTF-8,所以 Rust 迫使我们通过返回 Result&lt;String, FromUtf8Error&gt; 而不是 String 来考虑这一点。

    来自输入缓冲区

    use std::io::{self, Read};
    
    fn main() -> io::Result<()> {
        let mut buffer = String::new();
        let stdin = io::stdin();
        let mut handle = stdin.lock();
    
        handle.read_to_string(&mut buffer)?;
        Ok(())
    }
    

    或来自任何其他实现ToString trait 的类型

    由于String是底层的向量,它会表现出一些向量特征:

    • 指针:指针指向存储数据的内部缓冲区。
    • 长度:长度是当前存储在缓冲区中的字节数。
    • 容量:容量是缓冲区的大小,以字节为单位。因此,长度将始终小于或等于容量。

    并将一些属性和方法委托给向量:

    pub fn capacity(&self) -> usize {
      self.vec.capacity()
    }
    

    大多数示例都使用String::from,因此人们会困惑为什么要从另一个字符串创建字符串。

    篇幅较长,希望对你有所帮助。

    【讨论】:

      【解决方案6】:

      std::String 只是u8 的向量。您可以在source code 中找到它的定义。它是堆分配的且可增长的。

      #[derive(PartialOrd, Eq, Ord)]
      #[stable(feature = "rust1", since = "1.0.0")]
      pub struct String {
          vec: Vec<u8>,
      }
      

      str 是原始类型,也称为字符串切片。字符串切片具有固定大小。像 let test = "hello world" 这样的文字字符串具有 &amp;'static str 类型。 test 是对该静态分配字符串的引用。 &amp;str不能修改,例如,

      let mut word = "hello world";
      word[0] = 's';
      word.push('\n');
      

      str 确实有可变切片&amp;mut str,例如: pub fn split_at_mut(&amp;mut self, mid: usize) -&gt; (&amp;mut str, &amp;mut str)

      let mut s = "Per Martin-Löf".to_string();
      {
          let (first, last) = s.split_at_mut(3);
          first.make_ascii_uppercase();
          assert_eq!("PER", first);
          assert_eq!(" Martin-Löf", last);
      }
      assert_eq!("PER Martin-Löf", s);
      

      但是对 UTF-8 的一个小改动可以改变它的字节长度,并且切片不能重新分配它的引用对象。

      【讨论】:

      • 谢谢。我一直在寻找一个不依赖于String&amp;mut str,也就是说,没有to_string(),因为如果你已经有了String,为什么还要打扰str。这有效:let mut s: Box&lt;str&gt; = "Per Martin-Löf".into(); let (first, last) = s.split_at_mut(3); first.make_ascii_uppercase(); assert_eq!("PER Martin-Löf", &amp;*s);
      【解决方案7】:

      &amp;strString


      String:

      • Rust 拥有的字符串类型,字符串本身存在于堆中,因此是可变的,可以改变其大小和内容。
      • 因为当拥有该字符串的变量超出范围时,该字符串被拥有,所以堆上的内存将被释放。
      • String 类型的变量是胖指针(指针 + 相关元数据)
      • 胖指针为 3 * 8 字节(字大小)长,由以下 3 个元素组成:
        • 指向堆上实际数据的指针,它指向第一个字符
        • 字符串长度(字符数)
        • 堆上字符串的容量

      &amp;str:

      • Rust 非拥有的字符串类型,默认情况下是不可变的。字符串本身位于内存中的其他位置,通常位于堆或'static 内存中。
      • 因为当&amp;str 变量超出范围时字符串是非拥有的,所以字符串的内存不会被释放。
      • &amp;str 类型的变量是胖指针(指针 + 关联元数据)
      • 胖指针为 2 * 8 字节(字大小)长,由以下 2 个元素组成:
        • 指向堆上实际数据的指针,它指向第一个字符
        • 字符串长度(字符数)

      示例:

      use std::mem;
      
      fn main() {
          // on 64 bit architecture:
          println!("{}", mem::size_of::<&str>()); // 16
          println!("{}", mem::size_of::<String>()); // 24
      
          let string1: &'static str = "abc";
          // string will point to `static memory which lives through the whole program
      
          let ptr = string1.as_ptr();
          let len = string1.len();
      
          println!("{}, {}", unsafe { *ptr as char }, len); // a, 3
          // len is 3 characters long so 3
          // pointer to the first character points to letter a
      
          {
              let mut string2: String = "def".to_string();
      
              let ptr = string2.as_ptr();
              let len = string2.len();
              let capacity = string2.capacity();
              println!("{}, {}, {}", unsafe { *ptr as char }, len, capacity); // d, 3, 3
              // pointer to the first character points to letter d
              // len is 3 characters long so 3
              // string has now 3 bytes of space on the heap
      
              string2.push_str("ghijk"); // we can mutate String type, capacity and length will aslo change
              println!("{}, {}", string2, string2.capacity()); // defghijk, 8
      
          } // memory of string2 on the heap will be freed here because owner goes out of scope
      
      }
      

      【讨论】:

        【解决方案8】:

        简单来说,String 是存储在堆上的数据类型(就像Vec),您可以访问该位置。

        &amp;str 是一个切片类型。这意味着它只是对堆中某处已经存在的String 的引用。

        &amp;str 在运行时不进行任何分配。因此,出于记忆的原因,您可以使用&amp;str 而不是String。但是,请记住,在使用 &amp;str 时,您可能必须处理显式生命周期。

        【讨论】:

        • 堆中的某处——这并不完全准确。
        • 我的意思是str 是堆中已经存在的viewString
        • 我明白这就是您的意思,我是说这并不完全准确。 “堆”不是语句的必需部分。
        【解决方案9】:

        一些用法

        example_1.rs

        fn main(){
          let hello = String::("hello");
          let any_char = hello[0];//error
        }
        

        example_2.rs

        fn main(){
          let hello = String::("hello");
          for c in hello.chars() {
            println!("{}",c);
          }
        }
        

        example_3.rs

        fn main(){
          let hello = String::("String are cool");
          let any_char = &hello[5..6]; // = let any_char: &str = &hello[5..6];
          println!("{:?}",any_char);
        }
        

        Shadowing

        fn main() {
          let s: &str = "hello"; // &str
          let s: String = s.to_uppercase(); // String
          println!("{}", s) // HELLO
        }
        

        function

        fn say_hello(to_whom: &str) { //type coercion
             println!("Hey {}!", to_whom) 
         }
        
        
        fn main(){
          let string_slice: &'static str = "you";
          let string: String = string_slice.into(); // &str => String
          say_hello(string_slice);
          say_hello(&string);// &String
         }
        

        Concat

         // String is at heap, and can be increase or decrease in its size
        // The size of &str is fixed.
        fn main(){
          let a = "Foo";
          let b = "Bar";
          let c = a + b; //error
          // let c = a.to_string + b;
        }
        

        请注意,String 和 &str 是不同的类型,在 99% 的情况下,您应该只关心 &amp;str

        【讨论】:

          【解决方案10】:

          对于 C# 和 Java 人员:

          • 锈'String === StringBuilder
          • Rust 的 &amp;str ===(不可变)字符串

          我喜欢将&amp;str 视为字符串的视图,就像 Java / C# 中的一个内部字符串,您无法更改它,只能创建一个新字符串。

          【讨论】:

          • Java/C# 字符串和 Rust 字符串之间的最大区别在于 Rust 保证字符串是正确的 unicode,因此在字符串中获取第三个字符需要更多的思考,而不仅仅是“abc”[2] . (鉴于我们生活在一个多语言的世界,这是一件好事。)
          • This is incorrect。最高投票的答案已经解决了可变性的主题;请阅读以了解更多信息。
          • &mut str 非常少见,与 &str 不同。
          【解决方案11】:

          这里有一个快速简单的解释。

          String - 一个可增长的、可拥有的堆分配数据结构。可以强制转换为&amp;str

          str - (现在,随着 Rust 的发展)是可变的、固定长度的字符串,位于堆或二进制文件中。您只能通过字符串切片视图与str 作为借用类型进行交互,例如&amp;str

          使用注意事项:

          如果你想拥有或改变一个字符串,首选String - 例如将字符串传递给另一个线程等。

          如果您想获得字符串的只读视图,请首选&amp;str

          【讨论】:

          • This is incorrect。最高投票的答案已经解决了可变性的主题;请阅读以了解更多信息。
          猜你喜欢
          • 2015-02-16
          • 2011-04-21
          • 2017-06-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多