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


当前回答

str类似于String,而不是它的切片。

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开始的地址以及它需要多少字节。它比附加零字节要好,因为在编译期间计算是提前完成的。由于文本可以是任何大小,所以从类型系统的角度来看,它仍然是动态大小的。

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

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

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

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

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

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

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

例如,编译器为以下代码创建了一个内容为“Hello World”的连续二进制文件,尽管我们在“Hello世界”中使用了三个不同的文本:

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

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

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特性的任何其他类型

由于字符串是引擎盖下的矢量,因此它将显示一些矢量特性:

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

它将一些财产和方法委托给向量:

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

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

这是一本很长的书,希望能有所帮助。

其他回答

str类似于String,而不是它的切片。

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开始的地址以及它需要多少字节。它比附加零字节要好,因为在编译期间计算是提前完成的。由于文本可以是任何大小,所以从类型系统的角度来看,它仍然是动态大小的。

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

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

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

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

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

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

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

例如,编译器为以下代码创建了一个内容为“Hello World”的连续二进制文件,尽管我们在“Hello世界”中使用了三个不同的文本:

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

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

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特性的任何其他类型

由于字符串是引擎盖下的矢量,因此它将显示一些矢量特性:

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

它将一些财产和方法委托给向量:

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

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

这是一本很长的书,希望能有所帮助。

std::字符串只是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自身,mid:usize)->(&mut字符串,&mut字符串)

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的一个小改动可以改变它的字节长度,并且一个切片不能重新分配它的引用。

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

有趣的是,在运行时确实存在一个&str或任何其他指向str的指针,如Box<str>。这就是所谓的“胖指针”;它是一个带有额外信息的指针(在本例中是指它所指向的对象的大小),因此它的大小是它的两倍。事实上,&str非常接近字符串(但不是&String)。A&str是两个单词;一个指针指向str的第一个字节,另一个数字描述str的长度。

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

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

另一件事是Box<str>;它还拥有一个str,其运行时表示形式与&str相同,但它也拥有与&str不同的str,但它无法调整其大小,因为它不知道其容量,所以基本上Box<str>可以被视为一个固定长度的字符串,无法调整大小(如果要调整大小,可以始终将其转换为String)。

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

在类型级别上使用str主要是使用&str;它存在于类型级别,以便能够方便地书写特征。理论上,str作为一种类型的东西不需要存在,只需要&str,但这意味着需要编写很多额外的代码,这些代码现在可以是通用的。

&str非常有用,可以在不复制的情况下拥有一个字符串的多个不同子字符串;正如所说,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。字符串是拥有堆上实际完整str缓冲区的字符串,&str子字符串只是堆上缓冲区的胖指针。

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

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

&str在运行时不进行任何分配。因此,出于内存原因,可以在字符串上使用&str。但是,请记住,在使用&str时,您可能需要处理显式的生存期。

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

Rust字符串类似于std::String;它拥有内存并执行管理内存的肮脏工作。Rust&str就像char*(但有点复杂);它以同样的方式将我们指向块的开头,您可以获得指向std::string内容的指针。

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

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

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

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

以下代码允许您在没有副本的情况下使用文字本身(尽管是只读的):

let a: &str = "hello rust";