1. String
, &str
和 str
想要搞清楚这三者的差别,要从 str
开始说起。
1.1. str
和 &str
str
是字符串切片(string slice),是对部分连续的 UTF8 字符序列的引用。
所有的切片类型,例如 str
、[u8]
、[i32]
,都是 Unsized 的(不定长类型,或称动态大小类型,Dynamicly Sized Types, DST),它们占用的内存空间的具体大小在编译期是不可知的。
我们知道,变量都是存放在栈上的,而栈上是不允许存在一个大小不可知的数据的,因此如果我们想创建某个类型的变量,那么这个类型必须满足 Sized
约束,即定长类型。而对于这些 Unsized 类型,我们使用的时候就必须包裹一个 Sized
的间接层,比如引用 &
或者 Box
。
拿我们常见的 &str
、&[u8]
来举例:
-
&str
是对字符串切片的引用,其占用的内存空间是固定的,包含两个成员:一个是指针,指向连续的 u8 数据(存放着 UTF-8 字符),另一个是usize
类型,表示字节的数量。 -
其它切片引用,例如
&[T]
,占用的内存空间也是固定的,同样包含两个成员:一个指向连续数据的指针,和一个表示元素数量的计数。
同理,特征对象
dyn Trait
本身也是不定长类型,也要加上引用或者Box
才能操作。读到这里,你可能会有疑惑:绝大多数切片都是
[]
包起来的形式,但为什么要为字符串切片单独设计个str
呢?[char]
又是什么?先不急,我们后面会讲。
从代码角度来看,&str
可能长这样(注,只是说明,可能并非真实代码):
struct &str {
data: *const u8,
len: usize,
}
1.2. String
String
是一个可变的、堆上分配的 UTF-8 的 u8 缓冲区。这意味着 String
拥有一块堆上分配的内存的所有权,它用这块空间存放数据,并可以调整大小。
在标准库中我们可以看到 String
的原型:
pub struct String {
vec: Vec<u8>,
}
1.3. &str
与 String
的区别
在前面两个小节中,我们分别对这三种类型做出了定义:str
本质是字符串切片,&str
是字符串切片的引用,String
是对数据拥有所有权的堆上字节缓冲区。从这些定义出发,我们可以发现 &str
与 String
有如下几项重要的区别:
-
所有权:
String
拥有数据的所有权;而&str
只是切片,并不拥有所有权 -
空间拓展性:显然,切片作为(部分)引用,不能扩展其指向的数据空间;而
String
用有这块堆上数据的所有权,因此在需要的时候可以进行扩容。 -
底层数据存放位置:
String
的字符串数据一定存放在堆上;而&str
由于是切片,只是一个引用,其数据存放的位置要视借用对象而定。可以分成如下几种情况:- 引用的对象是字面量(String literal),则数据存放在 .data 段中;
- 引用的对象是数组,则数据存放在栈上;
- 引用的对象是
String
,则数据存放在堆上。 - 特别地,如果数组过于巨大,放在栈上会导致爆栈,此时 rust 编译器会转而将其存放到堆上,
&str
的数据存放位置也在堆上了。
1.4. 另一个区别:可修改性
由于 String
拥有数据的所有权,因此只要 String
变量是 mut
的,就可以随意对数据进行增删改,例如:
let mut s = String::from("Hello ");
s.push_str("rust");
但是,字符串切片,即使被定义成 &mut str
的形式,也只能经由 unsafe 的方法来修改数据。换言之,在 safe rust 中,&str
是无论如何都不能修改数据的。这一点与其他类型的切片大相径庭。
// u8 类型切片,可以修改引用的数据
let mut arr: [u8; 5] = [1, 2, 3, 4, 5];
let reference = &mut arr[1..2];
reference[0] = 1;
println!("{arr:?}"); // 打印:[1, 1, 3, 4, 5]
// 字符串切片,只能通过 unsafe 方法转为 &mut [u8] 来修改
let mut my_string = String::from("Hello, world!");
let my_str_slice: &mut str = &mut my_string[..];
let bytes: &mut [u8] = unsafe { my_str_slice.as_bytes_mut() };
bytes[7..12].copy_from_slice(b"Rust!");
println!("{}", my_string); // 打印:Hello, Rust!!
因此,&str
在 safe rust 中只能作为一种只读切片,尽管其他的切片类型都可以被定义成可写的。
1.5. &mut str
在 safe rust 中不能修改指向数据的原因
存在这个限制的主要原因是 &str
(还有 String
)强制采用 UTF-8 编码。UTF-8 编码中,一个字符的长度是不固定的,可能是 1~4 字节,视其第一个字节的前几位而定:
- 第一个 bit 是 0,则是一个单字节字符,即 ASCII
- 前三个 bit 是 110,则是个二字节字符
- 前四个 bit 是 1110,则是个三字节字符
- 前五个 bit 是 11110,则是个四字节字符
如果我们想通过 &mut str
修改指向的数据,那么可能会出现需要修改数据长度的情况,例如把某个单字节字符改成了多字节字符。然而,切片作为一种引用,并不具有数据的所有权,因而无权对数据的长度进行变动。所以,这种操作在 safe rust 中是不可行的。rust 设计者们的想法是,提供一个 unsafe 方法,让调用者自己保证修改后的结果依然是正确的 UTF-8 编码。
2. char
char
是 rust 中的另一种字符类型。前面我们提到, &str
和 String
都要求是 UTF-8 编码,但 char
是 unicode 编码,其每个字符长度是固定的 4 字节。
此外,char
本身就是一种基本类型,而 &str
和 String
本质上都是基于另一种基本类型—— u8
的。
由于以上这两点,char
与 &str
String
之间存在着显著的区别。当然,我们也能比较方便地在二者之间转换。
到底使用哪一方,主要还是视我们业务场景中需要使用到的编码类型来考虑。如果需要 unicode 编码,则我们需要使用 Vec<char>
和 &[char]
;如果 UTF-8 可以满足需求,那么就可以使用 String
和 &str
,从而节省内存空间。