繁体   English   中英

Rust 的 String 和 str 有什么区别?

[英]What are the differences between Rust's `String` and `str`?

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

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

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

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

  • 在堆分配的String内部String取消&str String数据的 &str 视图。

  • 在堆栈上:例如,以下创建一个堆栈分配的字节数组,然后&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之间的关系。


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

2从 Rust 1.2 开始, 动态大小的类型允许像Rc<str>这样的引用计数的 UTF-8 字节序列。 Rust 1.21 允许轻松创建这些类型。

我有 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";

它是str类似于String ,而不是它的切片,也称为&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 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

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

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

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

然而,Rust 采用了不同的方法:它使用切片。 切片存储str开始的地址以及它需要多少字节。 它比附加零字节要好,因为在编译期间会提前完成计算。

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

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

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

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

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

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

编译器更进一步。 如果在程序的多个位置使用相同的静态文本,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。 除此之外,它只是一个向量。

因此, 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<String, FromUtf8Error>而不是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特征的类型

由于String在底层是一个向量,它会表现出一些向量特性:

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

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

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

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

读了很久,希望对你有帮助。

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

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

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

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

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

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

另一件事是Box<str> this 也拥有一个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];

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

Rust &strString


String

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

&str :

  • Rust非拥有的 String 类型,默认情况下是不可变的。 字符串本身通常位于内存中的其他位置,通常位于堆或'static内存”中。
  • 因为当&str变量超出范围时 String 是非拥有的,所以不会释放字符串的内存。
  • &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

}

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

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

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

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

str确实有可变切片&mut str ,例如: pub fn split_at_mut(&mut self, mid: usize) -> (&mut str, &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 的一个小改动可以改变它的字节长度,并且一个 slice 不能重新分配它的引用对象。

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

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

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

一些用法

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% 的情况下,您只应该关心&str

对于 C# 和 Java 人员:

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

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

String是一个对象。

&str是指向对象一部分的指针。

在此处输入图像描述

在 Rust 中,str 是一种原始类型,表示一系列 Unicode 标量值,也称为字符串切片。 这意味着它是一个字符串的只读视图,并且它不拥有它指向的内存。 另一方面,String 是一种可增长、可变、自有的字符串类型。 这意味着当你创建一个 String 时,它会在堆上分配内存来存储字符串的内容,并且当 String 超出范围时它会释放这块内存。 因为 String 是可增长和可变的,所以您可以在创建 String 后更改它的内容。

通常,当您想要引用存储在另一个数据结构(例如 String)中的字符串切片时,会使用 str。 当您想要创建和拥有一个字符串值时使用字符串。

在这3种不同的类型中

let noodles = "noodles".to_string(); let oodles = &noodles[1..]; let poodles = "ಠ_ಠ"; // this is string literal
  • String 有一个可调整大小的缓冲区,其中包含 UTF-8 文本。 缓冲区是在堆上分配的,因此它可以根据需要或请求调整其缓冲区的大小。 在示例中,“noodles”是一个 String,它拥有一个八字节的缓冲区,其中七个正在使用中。 您可以将 String 视为 Vec,保证包含格式正确的 UTF-8; 实际上, String就是这样实现的。

  • &str是对其他人拥有的一系列 UTF-8 文本的引用:它“借用”了该文本。 在示例中,oodles 是一个 &str,指的是属于“noodles”的文本的最后六个字节,因此它表示文本“oodles”。 与其他切片引用一样, &str是一个fat pointer ,包含实际数据的地址及其长度。 您可以将&str想象成一个 &[u8],它保证包含格式正确的 UTF-8。

  • string literal是指预分配文本的&str ,通常与程序的机器代码一起存储在只读 memory 中。 在前面的示例中,poodles 是一个字符串文字,指向程序开始执行时创建并持续到程序退出的七个字节。

这就是它们在 memory 中的存储方式

在此处输入图像描述

参考:编程 Rust,作者:Jim Blandy、Jason Orendorff、Leonora FS Tindall

这是一个快速简单的解释。

String - 一种可增长的、可拥有的堆分配数据结构。 它可以被强制为&str

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

使用注意事项:

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM