3.1 切片布局
5 分钟阅读
译文 · 基于 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> 并得到共享或独占切片。
其他类型
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> 布局的图形表示:
| |
从图中应能立即看出,用 &[T] 而非 &Vec<T> 作为参数的一个优势:&[T] 间接层更少。
但还有其他原因:
&Vec<T>上有用的功能实际上都是&[T]的方法- 无法用
&[T]查看容量,但用&Vec<T>也无法改变容量
- 无法用
- 若参数为
&[T],可以接受指向非Vec<T>所拥有的数据的共享切片(如静态数据、数组的一部分、Box<[T]>内部等)- 因此用
&[T]严格且显著更通用
- 因此用
类似优势也适用于用 &str 代替 &String 等。
相比之下,&mut Vec<T> 能做很多事而 &mut [T] 不能,因此选哪个更取决于你需要对借用的数据做什么。
数组的图形化布局
数组的布局与切片相同,只是长度已知。
| |
因为 [T; N] 是 Sized 的,且长度是类型的一部分,指向它的指针(如 &[T; N])是普通的「瘦」指针,而非「宽」指针。但你也可以创建指向数组(或数组一部分)的 &[T],如上图所示。
函数参数应该用 &[T] 还是 &[T; N]?若不需要特定长度,也不试图根据数组的具体长度生成优化代码,通常应选 &[T]。