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 ,从而节省内存空间。
