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

时间:2014-06-11 08:29:35

标签: string rust

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

11 个答案:

答案 0 :(得分:335)

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之间的关系对于一般类型。


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

2 Dynamically sized types允许&str之类的内容,因为Rust 1.2之后的引用序列计数了UTF-8字节。 Rust 1.21允许轻松创建这些类型。

答案 1 :(得分:54)

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

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

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

String保留缓冲区并且非常实用。 &str是轻量级的,应该用于&#34;外观&#34;变成字符串。您可以搜索,拆分,解析甚至替换块,而无需分配新内存。

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

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

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

let a: &str = "hello rust";

答案 2 :(得分:36)

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

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

答案 3 :(得分:5)

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

有趣的是,在运行时确实存在&str或指向str的任何其他指针Box<str> 。这就是所谓的“胖指针”;它是一个具有额外信息的指针(在这种情况下,它是指向对象的大小),因此它的大小是其两倍。实际上,&str非常接近String(但不接近&String)。 &str是两个词;一个指针指向str的第一个字节,另一个指针描述str的字节数。

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

那么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创建泛型抽象;它存在于类型级别,以便能够方便地编写特征。从理论上讲,str作为类型的东西不需要存在,而仅需&str,但这意味着必须编写许多现在可以通用的额外代码。

&str非常有用,它能够具有String的多个不同子字符串而无需复制;如String在其管理的堆上{em>拥有 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子字符串只是指向堆上该缓冲区的胖指针。

答案 4 :(得分:3)

strString类似,而不是切片,也称为&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&之后。

因此,“ Hello World”表达式将返回一个胖指针,其中包含实际数据的地址及其长度。该指针将是我们处理实际数据的句柄。现在数据位于指针后面,编译器在编译时便知道其大小。

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

因此,“ Hello Word”表达式的返回值应反映出这两个特征,

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

您可能会问为什么将其类型写为str而不写为[u8],这是因为始终保证数据是有效的UTF-8序列。并非所有UTF-8字符都是单字节,有些不是4字节,也不是所有字节序列都是有效的UTF-8字符。因此[u8]是不准确的。

另一方面,String是u8字节的专用向量,换句话说,可调整大小的缓冲区保存UTF-8文本。我们之所以说专用是因为它不允许任意访问,并强制执行某些检查以确保数据始终是有效的UTF-8。缓冲区是在堆上分配的,因此可以根据需要或请求调整缓冲区的大小。

以下是在源代码中的定义:

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

您可以使用String结构创建字符串,但是vec是私有的,以确保有效性和正确的检查,因为并非所有字节流都是有效的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);

来自原始零件

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

来自输入缓冲区

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是引擎盖下的向量,它将表现出一些向量特征:

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

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

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

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

这是一本长篇小说,希望对您有所帮助。

答案 5 :(得分:3)

&strString


String

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

&str

  • Rust 非拥有的字符串类型,默认情况下是不可变的。字符串本身通常位于内存中的其他位置,通常位于堆或 '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

}

答案 6 :(得分:0)

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

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

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

答案 7 :(得分:0)

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

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

str是一种原始类型,也称为 string slice 。字符串切片的大小固定。像let test = "hello world"这样的文字字符串的类型为&'static strtest是对此静态分配的字符串的引用。 &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的微小更改可以更改其字节长度,并且分片无法重新分配其引用对象。

答案 8 :(得分:0)

对于C#和Java用户:

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

我喜欢将&str看作是字符串的视图,就像Java / C#中的固定字符串,您不能更改它,只能创建一个新字符串。

答案 9 :(得分:0)

一些用法

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

答案 10 :(得分:-2)

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

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

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

使用注意事项:

如果您想拥有或更改一个字符串(例如,将该字符串传递给另一个线程等),请首选String

如果要对字符串进行只读查看,请首选&str