String
是动态堆字符串类型,类似于Vec
:当您需要拥有或修改字符串数据时,请使用它。
str
是一个不可变的UTF-8字节序列,长度不确定,存储在内存中的某个位置。由于大小未知,只能通过指针来处理它。这意味着str
最常见的形式是&str
:对一些UTF-8数据的引用,通常称为“字符串切片”或者只是一个“切片”。切片只是对一些数据的视图,而这些数据可以位于任何地方,例如。
在静态存储中:字符串字面量"foo"
是一个&'static str
。数据被硬编码到可执行文件中,并在程序运行时加载到内存中。
在堆分配的String
中:String
解引用为&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允许轻松创建这些类型。
我有C ++背景,发现按照C++的想法考虑Rust中的String
和&str
非常有用:
String
类似于std::string
;它拥有内存并执行管理内存的工作。&str
类似于char*
(但更复杂);以与获取std::string
内容指针相同的方式将我们指向一个块的开头。它们中的任何一个都将消失吗?我不这么认为。它们有两个目的:
String
保存缓冲区,非常实用。 &str
轻量级,应用于查看字符串。您可以搜索、分割、解析甚至替换块而无需分配新内存。
&str
可以查看String
中的内容,因为它可以指向某个字符串文字。以下代码需要将文字字符串复制到由String
管理的内存中:
let a: String = "hello rust".into();
let a: &str = "hello rust";
string_view
是一种可怕的东西。想象一下 auto_ptr<T>
级别的糟糕。 - Tanveer Badarstr
类似于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
开始的地址和占用的字节数。它比附加零字节更好,因为计算是在编译期间提前完成的。
文本大小可以预先知道,但仍会随着底层数据的更改而发生变化,这使其具有动态大小。
如果我们回到“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
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
,这让人们感到困惑,为什么要从另一个字符串创建字符串。
这是一篇长文章,希望能有所帮助。
~str
现在是 Box<str>
。 - jv110~str
可以增长而Box<str>
不可增长。(~str
和~[T]
可以神奇地增长,与任何其他~
对象不同,这正是为什么引入String
和Vec<T>
的原因,以使规则变得简单明了和一致。) - Chris MorganString
是一种拥有类型...它对字符串的内容具有独占所有权;当它超出范围时,字符串的内容的内存将立即被释放。因此,任何子字符串都不能是String
类型...否则...当一个超出范围时,另一个将变得无效...所以,切片(子字符串)使用的类型是指向其他东西拥有的内容的引用——&str
...” - Nate Andersonstr
只是一种类型级别的东西;它只能在类型级别上进行推理,因为它是所谓的动态大小类型(DST)。str
占用的大小在编译时无法知道,取决于运行时信息,因此不能将其存储在变量中,因为编译器需要在编译时知道每个变量的大小。从概念上讲,str
只是一行字节,并保证形成有效的UTF-8。这一行有多大?在运行时之前没有人知道,因此它不能存储在变量中。&str
或指向str
的任何其他指针(例如Box<str>
)在运行时确实存在。这是所谓的“fat pointer”;它是一个带有额外信息的指针(在这种情况下,它指向的东西的大小),因此它的大小是原来的两倍。事实上,&str
与String
非常接近(但与&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];
我们有同一个字符串的两个不同的子串str
。 string
是拥有实际完整的str
缓冲区在堆上的字符串,而&str
子串只是指向该缓冲区的fat指针。
&str
和 String
字符串
:
字符串
的变量是fat指针(指针+关联元数据)&str
:
'static
内存中。&str
变量超出作用域时,字符串的内存不会被释放。
&str
类型的变量是胖指针(指针+关联元数据)。示例:
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
}
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);
- BlackShiftString
看起来与 Vec<u8>
重复了。两者之间的区别在于 String
始终包含有效的 UTF-8 编码文本,而 Vec<u8>
则可以容纳任何字节序列。 - kbolinoString
是一种存储在堆上的数据类型(就像Vec
),您可以访问该位置。
&str
是一种切片类型。这意味着它只是对已经存在于堆中的String
的引用。
&str
不会在运行时进行任何分配。因此,出于内存原因,您可以使用&str
而不是String
。但要记住,在使用&str
时,您可能需要处理显式生命周期。str
是堆中已经存在的 String
的 view
。 - 00imvj00In 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 afat 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
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
。
&str
由两部分组成:指向某些字节的指针和长度。” - mrec[u8; N]
不同,它没有静态地确定。 - huonString
的&str
切片。在垃圾收集语言中,切片可以存在于主要所有者消失之后,但在 Rust 中不行:编译器强制程序员显式选择如何处理它,例如,不共享内存(通过使用.to_owned()
创建一个单独的String
),或者像您所说共享内存(通过使用类似http://kimundi.github.io/owning-ref-rs/owning_ref/index.html#caching-a-subslice-of-a-string的东西缓存字符串的子切片)。 - huon