3.1 切片布局

译文 · 基于 Learning Rust

切片布局

原文链接: https://quinedot.github.io/rust-learning/misc-slice.html

在 论坛 上,人们经常会问为什么惯例上参数用 &[T] 而不是 &Vec<T>,或者更广泛地询问切片的布局。也会有人问 &str 和 String 等类型的类似问题。

本页旨在为这类问题提供有用的引用。

如果你愿意,可以跳到图形化布局。

切片到底是什么?

围绕切片的术语往往比较随意。本页会尽量更正式,但当你在其他地方读到「切片」时,请记住它可能指 [T]、&[T]、&mut [T],甚至指向 [T] 的其他指针类型(Box<[T]>、Arc<[T]> 等)。

这不仅见于非正式材料,也见于官方文档及其他技术材料。你只能根据上下文判断他们具体指的是哪一种或哪几种。

说完这些,为本页引入一些术语:

  • 切片 [T] 是内存中连续排列的一系列 T(一个接一个,且满足对齐要求)。长度仅在运行时可知;我们称其为动态大小类型(DST)、非大小类型,或不实现 Sized 的类型。

  • 共享切片 &[T] 是指向切片的共享引用。它是一种宽引用,由指向切片内存的指针和切片中的元素个数组成。

  • 独占切片 &mut [T] 与共享切片类似,但借用是独占的(因此你可以通过它覆盖元素等)。

  • 还有其他宽指针变体,如装箱切片(Box<[T]>)等;稍后还会提到更多。

注意:虽然切片是非大小的,但指向切片的宽指针(如 &[T])是有大小的。

切片在哪里?

切片可以在堆上,也可以在栈上、静态内存中或其他任何地方。类型本身并不「关心」它在哪里。因此,除非指针本身有进一步保证,否则你无法确定指向切片的指针指向何处。

例如,如果你有 Box<[T]>,那么其中的任何 T 都在堆上,因为这是 Box<_> 的保证。(注:若 T 是零大小类型,它们实际上并不存储在任何地方。)所以在那种特定情况下,我们可以说切片 [T] 在堆上。

Vec<_> 到底是什么?

Vec<T> 是在堆上拥有并存储连续 T 的可增长缓冲区。你可以概念性地将其视为拥有一个切片 [T](更准确地说,是 [MaybeUninit<T>])。你可以用范围索引 Vec<T> 并得到共享或独占切片。

Vec<_> 由指针、容量和长度组成。

其他类型

String 底层类似于 Vec<u8>,但有额外保证——即已初始化的字节是合法 UTF-8。&str 类似于 &[u8],有相同保证。你可以用范围索引 String 并得到 &str(或 &mut str)。与 [u8] 一样,str 是非大小的,因此你几乎总是在使用 &str 或其他指针。

因此 str 与 String 的关系,与 [T] 与 Vec<T> 的关系相同。还有其他类型对具有相同关系:

这些 std 类型通常具有 ToOwned 和 Borrow 关系。

更多可视为拥有型切片的数据结构还包括:

  • [T; N] 是编译时已知长度的数组(即固定大小数组)。它像切片([T]),但是 Sized 的,因为长度在编译时已知。长度也是类型的一部分。不可增长。

  • Box<[T]>,即「装箱切片」;类似于 Vec<T>,拥有 T 并在堆上连续存储。与 Vec<T> 不同,缓冲区不能通过 &mut Box<[T]> 增长(或缩小);需要分配新存储并移动元素。装箱切片的长度在运行时存储,编译时未知。因此像共享切片一样,由指针和长度组成。

  • Arc<[T]> 和 Rc<[T]> 是 Box<[T]> 的共享所有权变体。

  • 字符串类类型也有类似变体(Box<str>、Arc<Path>、Rc<OsStr> 等)

  • 还有其他组合(Box<[T; N]> 等)

你也可以为这些其他拥有型切片创建共享切片。

严格来说,单个 T 就像 [T; 1](内存布局相同)。因此若稍微眯眼看,每个拥有的 T 也是一种拥有型切片,只是编译时已知长度为 1。确实,你可以从 &T 和 &mut T 创建 &[T] 和 &mut [T](数组版本也可以)。

图形化布局

以下是切片、共享切片、Vec<T> 和 &Vec<T> 布局的图形表示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
+---+---+---+---+---+---+---+---+
| Pointer       | Length        | &[T] (或 &str、&Path、Box<[T]> 等)
+---+---+---+---+---+---+---+---+
  |
  V
+---+---+---+---+---+---+---+---+
| D | A | T | A | . | . | . | ......    [T] (或 str、Path 等)
+---+---+---+---+---+---+---+---+
  ^
  |
+---+---+---+---+---+---+---+---+---+---+---+---+
| Pointer       | Length        | Capacity      | Vec<T> (或 String、PathBuf 等)
+---+---+---+---+---+---+---+---+---+---+---+---+
  ^
  |
+---+---+---+---+
| Pointer       | &Vec<T> (或 &String、&PathBuf 等)
+---+---+---+---+

从图中应能立即看出,用 &[T] 而非 &Vec<T> 作为参数的一个优势:&[T] 间接层更少。

但还有其他原因:

  • &Vec<T> 上有用的功能实际上都是 &[T] 的方法
    • 无法用 &[T] 查看容量,但用 &Vec<T> 也无法改变容量
  • 若参数为 &[T],可以接受指向非 Vec<T> 所拥有的数据的共享切片(如静态数据、数组的一部分、Box<[T]> 内部等)
    • 因此用 &[T] 严格且显著更通用

类似优势也适用于用 &str 代替 &String 等。

相比之下,&mut Vec<T> 能做很多事而 &mut [T] 不能,因此选哪个更取决于你需要对借用的数据做什么。

数组的图形化布局

数组的布局与切片相同,只是长度已知。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
+---+---+---+---+---+---+---+---+
| Pointer       | Length        | &[T] (或 &str 等)
+---+---+---+---+---+---+---+---+
  |
  V
+---+---+---+---+---+---+---+
| D | A | T | A | . | . | . | [T; N]
+---+---+---+---+---+---+---+
  ^
  |
+---+---+---+---+
| Pointer       | &[T; N] (或 `Box<[T; N]>` 等)
+---+---+---+---+
  ^
  |
+---+---+---+---+
| Pointer       | &Box<[T; N]> (或 &&[T; N] 等)
+---+---+---+---+

因为 [T; N] 是 Sized 的,且长度是类型的一部分,指向它的指针(如 &[T; N])是普通的「瘦」指针,而非「宽」指针。但你也可以创建指向数组(或数组一部分)的 &[T],如上图所示。

函数参数应该用 &[T] 还是 &[T; N]?若不需要特定长度,也不试图根据数组的具体长度生成优化代码,通常应选 &[T]。

最后修改 August 23, 2026: 更新 (499855b16)