Rust的“String”和“str”有什么区别?

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

146300 次浏览

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

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

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

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

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

  • 在分配的堆中#0String的数据中的#0取消对#2视图的引用

  • 在堆栈上:例如,以下代码创建一个堆栈分配的字节数组,然后获取将该数据视为#0

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

2动态大小的类型允许像Rc<str>这样的东西用于自Rust 1.2以来的引用计数UTF-8字节序列。Rust 1.21允许轻松创建这些类型。

我有C++的背景,我发现从C++角度考虑String&str非常有用:

  • RustString就像std::string;它拥有内存并完成管理内存的肮脏工作。
  • Rust&str类似于char*(但更复杂一些);它将我们指向块的开头,就像您可以获得指向std::string内容的指针一样。

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

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

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

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

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

let a: &str = "hello rust";

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

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

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

它们实际上完全不同。首先,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基本上是&str0str,正如他们所说;它控制它,可以调整它的大小,并在它认为合适的时候重新分配它。所以String更接近&str而不是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];

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

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的微小更改可以更改其字节长度,并且切片无法重新分配其引用。

对于C#和Java:

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

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

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

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

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

使用注意事项:

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

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

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 │└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

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

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

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

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

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

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

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

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

你可能会问为什么它的类型写成str而不是[u8],这是因为数据总是保证是有效的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 unwraplet 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 trait的类型

由于String是引擎盖下的向量,它将表现出一些向量特征:

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

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

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

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

这是一个很长的阅读,希望它有帮助。

一些用法

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"; // &strlet s: String = s.to_uppercase(); // Stringprintln!("{}", s) // HELLO}

function

fn say_hello(to_whom: &str) { //type coercionprintln!("Hey {}!", to_whom)}

fn main(){let string_slice: &'static str = "you";let string: String = string_slice.into(); // &str => Stringsay_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

Rust&strString


String

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

&str

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

示例:

use std::mem;
fn main() {// on 64 bit architecture:println!("{}", mem::size_of::<&str>()); // 16println!("{}", 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 changeprintln!("{}, {}", string2, string2.capacity()); // defghijk, 8
} // memory of string2 on the heap will be freed here because owner goes out of scope
}

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

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