特殊大小类型

多数时候我们认为类型有一个固定的正的大小。然而并不总是这样。

动态大小类型(Dynamically Sized Types (DSTs))

事实上Rust支持动态大小类型(DSTs):没有一个已知的大小或对齐值的类型。表面上这有点荒谬:Rust必须知道大小和对齐什么的才能正常处理它们!在这一点上,DST并不是正常的类型。因为他们缺乏静态可知的大小,这些类型只能存在于一些指针之后。任何指向DST的指针也因此变成了一个指针,它包含指针和使其“完整”的信息(下面介绍更多细节)。

语言提供了两个主要的DST:trait对象和切片(slice)。

一个trait对象表现为一些实现了它指定trait的类型。它实际的原始类型被一个包含使用这个类型所需的所有信息的虚函数表(vtable)所清除,通过动态反射的支持。这就是一个trait对象的完整信息:一个它的vtable的指针。

切片仅仅是一些连续储存的一个视图 — 代表性的有数组或Vec。一个切片的完整信息仅仅是它指向的元素的数量。

事实上结构体可以在他们的最后一个字段上存储一个单独的DST,不过这也使得它变成了一个DST:

  1. // Can't be stored on the stack directly
  2. struct Foo {
  3. info: u32,
  4. data: [u8],
  5. }

注意:Rust 1.0中,如果结构体DST的最后一个字段有一个基于它的对齐的可变位置则会出错

零大小类型(Zero Sized Types (ZSTs))

Rust也允许指定不占用空间的类型:

  1. struct Foo; // No fields = no size
  2. // All fields have no size = no size
  3. struct Baz {
  4. foo: Foo,
  5. qux: (), // empty tuple has no size
  6. baz: [u8; 0], // empty array has no size
  7. }

很明显,仅就ZST本身而言,是非常没有用的。然而就如同Rust中很多稀奇的布局选择一样:它的潜力在泛型环境中得以展现:Rust大体上理解人额好产生和存储ZST的操作可以被缩减为一个no-op。首先,存储它甚至就没有意义 — 它并不占用任何空间。另外这个类型就只有一个值(0?),所以任何读取它的操作只会凭空产生(从aether中。。。) — 这也是一个no-op因为它不占有空间。

其中最极端的例子是当他们是Map和Set。给定一个Map<Key, Value>,实现一个Set<Key>仅仅作为Map<Key, UselessJunk>的简单封装是很常见的。在很多语言中,他们会强制为无用垃圾(ZST)分配空间并仅仅为了消除而存储和加载它。证明这些操作是无意义的分析对编译器来说是很困难的。

而在Rust中,我们可以就这么说Set<Key> = Map<Key, ()>。现在Rust静态的知晓了每一个读取和存储都是无意义的,并没有分配任何大小的空间。这导致单态代码基本上是一个HashSet的自定义实现,它没有HashMap那样不得不支持值的开销。

安全代码并不需要担心ZST,不过不安全代码必须考虑没有大小的类型所造成的后果。特别的,(ZST)的指针位移是no-op,并且标准分配器(包括jemalloc,Rust默认使用的分配器)通常认为对一个分配传递0是未定义行为。

空白类型(Empty Types)

Rust甚至也允许类型被声明为不能被实例化。这些类型只能在类型层面被考虑,绝不能在值的层面。空白类型被声明为一个没有变量的枚举:

  1. enum Void {} // No variants = EMPTY

空白类型甚至比ZST更极端。Void类型的主要动机是类型层面的不可达性。实际上不可能通过返回一个Result<T, Void>在类型层面传递它。API的调用者可以大胆打开这个Result,因为可以静态的知晓这个值不可能是一个Err,因为这会要求我们提供一个Void类型的值。

Rust原则上可以基于这个事实进行一些有趣的分析和优化。例如,Result<T, Void>可以仅仅表现为T,因为Err实际不可能存在。如下的代码也编译:

  1. enum Void {}
  2. let res: Result<u32, Void> = Ok(0);
  3. // Err doesn't exist anymore, so Ok is actually irrefutable.
  4. let Ok(num) = res;

不过所有这些花招现在都不管用了,所以所有Void类型能带给你的就是确信这种情况是静态不可能的。(能不废话吗。。。)

空白类型的最后一个微妙的细节是构建一个指向它的裸指针实际上是有效的,不过解引用它是未定义行为,因为这根本没有意义。也就是说,你可以用*const Void来代表C语言的void *,不过通过使用例如*const ()之类的并不一定能得到什么东西,它可以安全的随意解引用。