Rust的`String`和`str`有什么区别?

984
为什么Rust同时拥有Stringstr?它们之间有什么区别,何时应该使用其中之一而不是另一个?它们中的一个是否会被弃用?
15个回答

1115

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

str是一个不可变的UTF-8字节序列,长度不确定,存储在内存中的某个位置。由于大小未知,只能通过指针来处理它。这意味着str最常见的形式是&str:对一些UTF-8数据的引用,通常称为“字符串切片”或者只是一个“切片”。切片只是对一些数据的视图,而这些数据可以位于任何地方,例如。

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

  • 在堆分配的StringString解引用为&str视图,指向String的数据。

  • 在栈上:例如,以下代码创建了一个栈分配的字节数组,然后将其作为 &str的视图获取:

    use std::str;
    
    let x: [u8; 3] = [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 都是不可变的。通常情况下,改变需要写入比之前更多或更少的字节(例如,用一个 a(1个字节)替换为一个 ä(2个或更多字节)需要在 str 中腾出更多空间)。有特定的方法可以直接修改一个 &mut str,主要是那些只处理ASCII字符的方法,比如 make_ascii_uppercase

2 动态大小类型 允许使用 Rc<str> 来表示一系列引用计数的UTF-8字节,自Rust 1.2以来就支持。Rust 1.21允许轻松创建这些类型。


33
“sequence of UTF-8 bytes (长度未知)” - 这个描述已经过时了吗?这份文档中写到:“&str由两部分组成:指向某些字节的指针和长度。” - mrec
41
它并不过时(那个表述一直相当稳定),只是有点不太准确:与例如[u8; N]不同,它没有静态地确定。 - huon
13
@mrec 在编译时未知,不能对其大小进行假设,例如在创建堆栈框架时。因此,它通常被视为引用(reference),而引用在编译时具有已知大小,即指针的大小。 - Sekhat
7
@cjohansson 静态分配的对象通常不存储在堆栈上,而是存储在它们自己的内存区域中。翻译完毕。 - Brennan Vincent
4
@lxx,不行,Rust 的所有权和借用机制会生效:编译器不允许您持有指向超出其作用域并被释放的 String&str 切片。在垃圾收集语言中,切片可以存在于主要所有者消失之后,但在 Rust 中不行:编译器强制程序员显式选择如何处理它,例如,不共享内存(通过使用 .to_owned() 创建一个单独的 String),或者像您所说共享内存(通过使用类似http://kimundi.github.io/owning-ref-rs/owning_ref/index.html#caching-a-subslice-of-a-string的东西缓存字符串的子切片)。 - huon
显示剩余6条评论

253

我有C ++背景,发现按照C++的想法考虑Rust中的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";

41
像 string_view 一样? - Abhinav Gauniyal
19
类似于 string_view 但是内置于语言中并且经过借用检查。 - locka
1
string_view 是一种可怕的东西。想象一下 auto_ptr<T> 级别的糟糕。 - Tanveer Badar

151

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

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

"Hello World"

这段文本需要被存储在某个地方,因此它会和程序的机器码一起作为字节序列([u8])存储在可执行文件的数据部分中。

┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│  H  │  e  │  l  │  l  │  o  │     │  W  │  o  │  r  │  l  │  d  │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│  721011081081113287111114108100 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

由于文本可以是任意长度,因此它们是动态大小的。

既然我们已经存储了文本,我们需要一种访问它的方式,这就是切片的作用。

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

让我们解释一下动态大小的含义。

一些编程语言(如C)在其字符串末尾附加一个零字节(\0),并记录起始地址。要确定字符串的长度,程序必须从起始位置遍历原始字节,直到找到这个零字节。

但是Rust采用了不同的方法:它使用切片。切片存储str开始的地址和占用的字节数。它比附加零字节更好,因为计算是在编译期间提前完成的。

文本大小可以预先知道,但仍会随着底层数据的更改而发生变化,这使其具有动态大小。

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

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

因此,“Hello World”表达式的返回值应反映这两个特征,它确实如此:

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>,
}

作为向量,它是像任何其他向量值一样在堆上分配和可调整大小的。

然而,如果你仔细看,你会发现vec字段是被保持私有的。被保持私有意味着,我们不能直接创建一个String实例,但是可以通过提供的方法来创建。之所以将其保持私有是因为并不是所有的字节流都能产生有效的utf-8字符,直接与底层字节进行交互可能会破坏数据。通过这种受控访问编译器强制执行数据有效并保持有效。

类型定义中的"specialized"一词指的就是这个特性,即不允许任意访问但通过受控访问对数据进行某些检查以提供某些保证的特性。除此之外,它只是一个向量。

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

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);

请注意,使用String.from将创建一个str并将其内容复制到堆分配的向量中。如果我们检查可执行二进制文件,就会在数据部分看到带有“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,这让人们感到困惑,为什么要从另一个字符串创建字符串。

这是一篇长文章,希望能有所帮助。


1
太长了但是很棒的阅读,谢谢。我感觉对Rust中的字符串有了更多的理解。希望我确实如此! - ed9w2in6
@snnsnn 感谢你提供这个信息丰富的答案。我是一名有经验的前端开发者。现在,我想学习Rust,但不知道有哪些好的学习资源。你能否帮助我提供一些学习Rust的资源呢?另外,我对C/C++也没有任何基础。所以,我也希望能够理解硬件层面的概念。 - Vishal
@Vishal 你说的不知道有什么好资源是什么意思?是找不到任何资源,还是觉得找到的资源不够有帮助,因为即使简单搜索也能返回很多资源。如果你喜欢这段文字,它是我正在写的一本书的一部分,我已经完成了大约8章,但是没有时间完成整本书。 - snnsnn
@snnsnn 我的意思是,我找到了很多资源,但没有一个资源深入到硬件层面,而那些能让我更深入了解的资源又假设我已经具备一定的Rust知识。所以,我有点困惑应该选择哪条路线。因此,我请求你如果有好的资源,请分享给我。我喜欢你分享的内容,非常渴望在你的书发布时购买,届时我会继续从我找到的资源中学习。 - Vishal
很高兴听到这个消息,我完全明白你的意思,而且这正是我在这本书中的目标,展示代码在各个层面上的运作方式,包括API、API与硬件之间的抽象层以及硬件本身。它使用了汇编输出、编译器代码的实现细节、语法糖以及所有需要了解的内容。甚至还涉及了与操作系统相关的内容、数据通道和内存、RAM。我甚至为此阅读了CPU规格说明书。我不知道有任何资源能提供如此详细的信息。这是一项巨大的工作,虽然不是我的首要任务,但是你的评论重新点燃了我的渴望。我会让你知道的 :) - snnsnn

72

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

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


2
从技术上讲,曾经的 ~str 现在是 Box<str> - jv110
16
不行,因为~str可以增长而Box<str>不可增长。(~str~[T]可以神奇地增长,与任何其他~对象不同,这正是为什么引入StringVec<T>的原因,以使规则变得简单明了和一致。) - Chris Morgan
3
我也很喜欢你的博客文章“Rust FizzBuzz”,“两种类型的字符串?这是什么?”特别是:“String是一种拥有类型...它对字符串的内容具有独占所有权;当它超出范围时,字符串的内容的内存将立即被释放。因此,任何子字符串都不能是String类型...否则...当一个超出范围时,另一个将变得无效...所以,切片(子字符串)使用的类型是指向其他东西拥有的内容的引用——&str...” - Nate Anderson

70
它们实际上完全不同。首先,str只是一种类型级别的东西;它只能在类型级别上进行推理,因为它是所谓的动态大小类型(DST)。str占用的大小在编译时无法知道,取决于运行时信息,因此不能将其存储在变量中,因为编译器需要在编译时知道每个变量的大小。从概念上讲,str只是一行字节,并保证形成有效的UTF-8。这一行有多大?在运行时之前没有人知道,因此它不能存储在变量中。
有趣的是,&str或指向str的任何其他指针(例如Box<str>)在运行时确实存在。这是所谓的“fat pointer”;它是一个带有额外信息的指针(在这种情况下,它指向的东西的大小),因此它的大小是原来的两倍。事实上,&strString非常接近(但与&String不同)。&str有两个字;一个指向str的第一个字节的指针,另一个数字描述了str有多长。
与所说的相反,str不需要是不可变的。如果您可以获得&mut str作为对str的独占指针,则可以对其进行修改,并且所有安全函数都保证维护UTF-8约束,因为如果违反该约束,则我们具有未定义的行为,因为库假定此约束为真并且不检查它。
那么什么是String?这是三个字;其中两个与&str相同,但它添加了第三个单词,即str缓冲区在堆上的容量(str不一定在堆上)。在填充之前,它管理缓冲区,必须重新分配。基本上,String拥有一个str,它控制并可以在适当时调整大小并重新分配它。因此,String更接近于&str而不是str
另一个事情是 Box<str>,它也拥有一个 str,其运行时表示与 &str 相同,但它还拥有 str,不像 &str,但它不能调整大小,因为它不知道其容量,所以基本上可以将 Box<str> 视为固定长度的 String,无法调整大小(如果想要调整大小,可以随时将其转换为 String)。 [T]Vec<T> 之间存在非常相似的关系,只是没有 UTF-8 约束,并且可以容纳任何大小不是动态的类型。
在类型级别上使用 str 主要是为了使用 &str 创建通用抽象;它存在于类型级别上,以便能够方便地编写 trait。理论上,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];

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


2
“它无法存储在变量中,因为编译器需要在编译时知道每个变量的大小。” > 您能解释一下为什么编译器不能生成使用运行时关于字符串长度的信息的二进制代码吗?这是一种 Rust 设计限制吗? - Ilya Loskutov
2
@Mergasov 可以这样做,但从性能角度来看,这将极其低效,并且会完全改变函数调用约定,这些约定依赖于已知此信息。在编译时知道函数调用堆栈的大小和每个变量的位置非常重要,这是生成高效程序的原因,也是为什么堆栈比堆快几个数量级的原因。在这种情况下,更容易的方法是将其放在指针后面的堆上;这本质上是将堆栈转换为第二个堆。 - Zorf

37

Rust &strString


字符串:

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

&str:

Rust的非所有权String类型默认是不可变的。字符串本身通常存储在内存中的其他位置,例如堆或'static内存中。
因为String是非所有权的,当&str变量超出作用域时,字符串的内存不会被释放。 &str类型的变量是胖指针(指针+关联元数据)。
胖指针的长度为2 * 8字节(字大小),包含以下两个元素:
  • 指向堆上实际数据的指针,它指向第一个字符
  • 字符串的长度(字符数)

示例:

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 throughout 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 also change
        println!("{}, {}", string2, string2.capacity()); // defghijk, 8

    } // memory of string2 on the heap will be freed here because owner goes out of scope

}

19

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 的小改动就可以改变它的字节长度,而切片无法重新分配其引用。


谢谢。我正在寻找一个不依赖于String&mut str,也就是说,没有to_string(),因为如果你已经有了String,为什么还要费心去处理str呢。这个可以用:let mut s: Box<str> = "Per Martin-Löf".into(); let (first, last) = s.split_at_mut(3); first.make_ascii_uppercase(); assert_eq!("PER Martin-Löf", &*s); - BlackShift
1
开头的句子会让 String 看起来与 Vec<u8> 重复了。两者之间的区别在于 String 始终包含有效的 UTF-8 编码文本,而 Vec<u8> 则可以容纳任何字节序列。 - kbolino

13
简单来说,String是一种存储在堆上的数据类型(就像Vec),您可以访问该位置。 &str是一种切片类型。这意味着它只是对已经存在于堆中的String的引用。 &str不会在运行时进行任何分配。因此,出于内存原因,您可以使用&str而不是String。但要记住,在使用&str时,您可能需要处理显式生命周期。

4
在堆中某个地方 —— 这并不完全准确。 - Shepmaster
我的意思是,str 是堆中已经存在的 Stringview - 00imvj00
6
我明白你的意思,我想说那不完全准确。“堆”不是语句中必需的部分。 - Shepmaster

9

In these 3 different types

let noodles = "noodles".to_string();
let oodles = &noodles[1..];
let poodles = "ಠ_ಠ"; // this is string literal
  • A String has a resizable buffer holding UTF-8 text. The buffer is allocated on the heap, so it can resize its buffer as needed or requested. In the example, "noodles" is a String that owns an eight-byte buffer, of which seven are in use. You can think of a String as a Vec that is guaranteed to hold well-formed UTF-8; in fact, this is how String is implemented.

  • A &str is a reference to a run of UTF-8 text owned by someone else: it “borrows” the text. In the example, oodles is a &str referring to the last six bytes of the text belonging to "noodles", so it represents the text “oodles.” Like other slice references, a &str is a fat pointer, containing both the address of the actual data and its length. You can think of a &str as being nothing more than a &[u8] that is guaranteed to hold well-formed UTF-8.

  • A string literal is a &str that refers to preallocated text, typically stored in read-only memory along with the program’s machine code. In the preceding example, poodles is a string literal, pointing to seven bytes that are created when the program begins execution and that last until it exits.

This is how they are stored in memory

在这里输入图片描述

参考书目:《Rust编程》, 作者:Jim Blandy, Jason Orendorff, Leonora F . S. Tindall


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%的情况,你只需要关注 &str

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接