字符串切片
在之前的章节中,你已经见过不少字符串字面量被用于代码中,比如 "待办事项"
或 "票据描述"
。它们后面常常跟着 .to_string()
或 .into()
的调用。现在是时候理解这么做的原因了!
字符串字面量
通过将原始文本包含在双引号中,你可以定义一个字符串字面量:
#![allow(unused)] fn main() { let s = "你好,世界!"; }
s
的类型是 &str
,即指向字符串切片的引用。
内存布局
&str
和 String
是不同的类型——它们不能互换。让我们回顾一下之前探索过的 String
的内存布局。如果我们运行:
#![allow(unused)] fn main() { let mut s = String::with_capacity(5); s.push_str("Hello"); }
在内存中会得到这样的情况:
+---------+--------+----------+
Stack | pointer | length | capacity |
| | | 5 | 5 |
+--|------+--------+----------+
|
|
v
+---+---+---+---+---+
Heap: | H | e | l | l | o |
+---+---+---+---+---+
如果你还记得,我们也检查过 &String
在内存中的布局:
--------------------------------------
| |
+----v----+--------+----------+ +----|----+
| pointer | length | capacity | | pointer |
| | | 5 | 5 | | |
+----|----+--------+----------+ +---------+
| s &s
|
v
+---+---+---+---+---+
| H | e | l | l | o |
+---+---+---+---+---+
&String
指向存储 String
元数据的内存位置。如果我们跟随指针,就能到达堆分配的数据,特别是字符串的第一个字节 你
。
如果我们想要一个类型来表示 s
的子字符串呢?比如在 "你好"
中的 "好世界"
?
字符串切片
&str
是对字符串的一个视图,是对存储在别处的 UTF-8 字节序列的引用。例如,你可以像这样从 String
创建一个 &str
:
#![allow(unused)] fn main() { let mut s = String::with_capacity(5); s.push_str("你好"); // 从 `String` 创建一个字符串切片引用,跳过第一个字节。 let slice: &str = &s[1..]; }
在内存中,它看起来像这样:
s slice
+---------+--------+----------+ +---------+--------+
Stack | pointer | length | capacity | | pointer | length |
| | | 5 | 5 | | | | 4 |
+----|----+--------+----------+ +----|----+--------+
| s |
| |
v |
+---+---+---+---+---+ |
Heap: | H | e | l | l | o | |
+---+---+---+---+---+ |
^ |
| |
+--------------------------------+
slice
在堆栈上存储了两部分信息:
- 指向切片第一个字节的指针。
- 切片的长度。
slice
并不拥有数据,它只是指向数据:它是对 String
堆分配数据的引用。当 slice
被丢弃时,堆分配的数据不会被释放,因为它仍由 s
所拥有。这就是为什么 slice
没有 容量
字段的原因:它不拥有数据,所以不需要知道为数据分配了多少空间;它只关心它所引用的数据。
&str
与 &String
的区别
一般来说,当你需要对文本数据的引用时,优先使用 &str
而不是 &String
。&str
更加灵活,并且通常被认为是 Rust 代码中更符合习惯的用法。
如果一个方法返回 &String
,你是在承诺某处存在与你返回引用所匹配的堆分配的 UTF-8 文本。相反,如果一个方法返回 &str
,则拥有更多的灵活性:你只是说某处有一段文本数据,并且其中的一部分与你需要的匹配,因此你返回对这部分的引用。
参考练习
- 本节的练习位于
exercises/04_traits/05_str_slice