特殊大小的类型
大多数情况下,我们期望类型具有静态已知且正的大小。但这在 Rust 中并非总是如此。
动态大小的类型 (DSTs)
Rust 支持动态大小的类型 (DSTs):这类类型没有静态已知的大小或对齐方式。表面上看,这有点令人费解:Rust 必须知道某个事物的大小和对齐方式才能正确地处理它!在这方面,DSTs 不是普通类型。由于它们缺乏静态已知的大小,这些类型只能存在于指针之后。因此,指向 DST 的任何指针都会变成一个宽指针,它由指针和“补全”它们的信息组成(详见下文)。
语言暴露了两种主要的 DST
Trait 对象表示实现其指定 Trait 的某种类型。原始的确切类型被擦除,取而代之的是通过一个包含使用该类型所需所有信息的虚表(vtable)进行运行时反射。补全 Trait 对象指针的信息是虚表指针。被指向对象的运行时大小可以从虚表中动态请求。
切片仅仅是对某个连续存储区域(通常是数组或 Vec)的一个视图。补全切片指针的信息就是它指向的元素数量。被指向对象的运行时大小就是元素的静态已知大小乘以元素的数量。
结构体实际上可以直接在其最后一个字段中存储单个 DST,但这也会使其成为 DST
#![allow(unused)] fn main() { // Can't be stored on the stack directly struct MySuperSlice { info: u32, data: [u8], } }
不幸的是,如果无法构造这种类型,它在很大程度上是无用的。目前唯一受良好支持的创建自定义 DST 的方法是使您的类型泛型化并执行去大小强制转换(unsizing coercion)
struct MySuperSliceable<T: ?Sized> { info: u32, data: T, } fn main() { let sized: MySuperSliceable<[u8; 8]> = MySuperSliceable { info: 17, data: [0; 8], }; let dynamic: &MySuperSliceable<[u8]> = &sized; // prints: "17 [0, 0, 0, 0, 0, 0, 0, 0]" println!("{} {:?}", dynamic.info, &dynamic.data); }
(是的,目前自定义 DST 在很大程度上是一个半成品功能。)
零大小类型 (ZSTs)
Rust 还允许指定不占用空间的类型
#![allow(unused)] fn main() { struct Nothing; // No fields = no size // All fields have no size = no size struct LotsOfNothing { foo: Nothing, qux: (), // empty tuple has no size baz: [u8; 0], // empty array has no size } }
就其本身而言,零大小类型 (ZSTs) 由于显而易见的原因相当无用。然而,正如 Rust 中许多有趣的布局选择一样,它们的潜力在泛型上下文中得以实现:Rust 在很大程度上理解,任何产生或存储 ZST 的操作都可以简化为无操作(no-op)。首先,存储它根本就没有意义——它不占用任何空间。此外,那种类型只有一个值,所以任何加载它的操作都可以凭空生成它——这同样是一个无操作,因为它不占用任何空间。
最极端的例子之一是集合(Sets)和映射(Maps)。给定一个 Map<Key, Value>,通常会将 Set<Key> 实现为 Map<Key, UselessJunk> 的一个薄层包装。在许多语言中,这需要为 UselessJunk 分配空间,并进行存储和加载 UselessJunk 的工作,结果却将其丢弃。证明这是不必要的对于编译器来说将是一个困难的分析。
然而在 Rust 中,我们可以直接说 Set<Key> = Map<Key, ()>。现在 Rust 静态地知道每次加载和存储都是无用的,并且没有任何分配具有大小。结果是,单态化(monomorphized)的代码基本上是一个定制的 HashSet 实现,没有任何 HashMap 为了支持值而必须承担的开销。
安全代码无需担心 ZSTs,但不安全代码必须注意无大小类型带来的后果。特别是,指针偏移是无操作(no-op),并且分配器通常要求非零大小。
请注意,与所有其他引用一样,对 ZSTs 的引用(包括空切片)必须是非空且对齐良好的。解引用一个指向 ZST 的空指针或未对齐指针是未定义行为,就像对任何其他类型一样。
空类型
Rust 还允许声明甚至无法实例化的类型。这些类型只能在类型层面讨论,而不能在值层面讨论。空类型可以通过指定一个没有变体的枚举来声明
#![allow(unused)] fn main() { enum Void {} // No variants = EMPTY }
空类型比 ZSTs 甚至更为边缘。空类型的主要用例是类型层面的不可达性。例如,假设一个 API 通常需要返回一个 Result,但某个特定情况实际上是不会失败的。通过返回 Result<T, Void>,实际上可以在类型层面传达这一点。API 的消费者可以自信地解开这样的 Result,因为他们知道这个值在静态上不可能是 Err,因为这需要提供一个 Void 类型的值。
原则上,Rust 可以基于这一事实进行一些有趣的分析和优化。例如,Result<T, Void> 仅表示为 T,因为 Err 情况实际上并不存在(严格来说,这只是一种不保证的优化,因此例如将一个强制转换(transmute)为另一个仍然是未定义行为)。
以下代码也能编译
#![allow(unused)] fn main() { enum Void {} let res: Result<u32, Void> = Ok(0); // Err doesn't exist anymore, so Ok is actually irrefutable. let Ok(num) = res; }
关于空类型的最后一个微妙细节是,指向它们的裸指针实际上是有效的构造,但解引用它们是未定义行为,因为这没有意义。
我们不建议使用 *const Void 来模拟 C 语言的 void* 类型。许多人开始这样做,但很快就遇到了麻烦,因为 Rust 实际上没有任何安全防护措施来阻止使用不安全代码实例化空类型,如果这样做,就是未定义行为。这尤其成问题,因为开发者习惯于将裸指针转换为引用,而构造 &Void 也是未定义行为。
*const ()(或等效类型)对于 void* 来说工作得相当好,并且可以毫无安全问题地转换为引用。它仍然不能阻止您尝试读取或写入值,但至少它编译为无操作(no-op),而不是未定义行为。
外部类型 (Extern Types)
存在一个已接受的 RFC,旨在添加具有未知大小的适当类型,称为外部类型(extern types),这将允许 Rust 开发者更精确地建模 C 语言的 void* 和其他“已声明但从未定义”的类型。然而,截至 Rust 2018 版本,该特性由于 size_of_val::<MyExternType>() 应如何行为而陷入僵局。