奇特尺寸类型

大多数时候,我们期望类型具有静态已知且为正的大小。但在 Rust 中情况并非总是如此。

动态大小类型 (DSTs)

Rust 支持动态大小类型 (DSTs):没有静态已知大小或对齐方式的类型。从表面上看,这有点荒谬:Rust *必须* 知道某些东西的大小和对齐方式才能正确地使用它!在这方面,DSTs 不是普通类型。因为它们缺少静态已知的大小,所以这些类型只能存在于指针之后。任何指向 DST 的指针因此都会变成一个 *宽* 指针,其中包含指针和“完成”它们的信息(更多内容见下文)。

该语言公开了两个主要的 DST

  • trait 对象:dyn MyTrait
  • 切片:[T]str 以及其他

trait 对象表示实现其指定 trait 的某种类型。确切的原始类型被 *擦除*,取而代之的是运行时反射,其中包含一个 vtable,其中包含使用该类型所需的所有信息。完成 trait 对象指针的信息是 vtable 指针。可以通过 vtable 动态请求被指向的运行时大小。

切片只是对一些连续存储的视图,通常是数组或 Vec。完成切片指针的信息只是它指向的元素的数量。被指向的运行时大小只是元素的静态已知大小乘以元素的数量。

结构体实际上可以将单个 DST 直接存储为其最后一个字段,但这也会使它们成为 DST

#![allow(unused)]
fn main() {
// Can't be stored on the stack directly
struct MySuperSlice {
    info: u32,
    data: [u8],
}
}

尽管没有构造它的方法,这种类型在很大程度上是无用的。目前,创建自定义 DST 的唯一正确支持的方法是使你的类型通用并执行 *取消大小强制转换*

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 的操作都可以简化为无操作。首先,存储它甚至没有意义 —— 它不占用任何空间。此外,该类型只有一个值,因此任何加载它的东西都可以从虚空中产生它 —— 这也是一个无操作,因为它不占用任何空间。

这方面最极端的例子之一是集合和映射。给定一个 Map<Key, Value>,通常将 Set<Key> 实现为只是 Map<Key, UselessJunk> 的一个薄包装器。在许多语言中,这需要为 UselessJunk 分配空间并执行存储和加载 UselessJunk 的工作,只是为了将其丢弃。证明这是不必要的对于编译器来说将是一个困难的分析。

但是,在 Rust 中,我们可以简单地说 Set<Key> = Map<Key, ()>。现在,Rust 静态地知道每次加载和存储都是无用的,并且任何分配都没有大小。结果是,单态化代码基本上是一个 HashSet 的自定义实现,没有 HashMap 必须支持值的任何开销。

安全代码无需担心 ZST,但是 *不安全* 代码必须小心没有大小的类型带来的后果。特别是,指针偏移是无操作,并且分配器通常需要非零大小

请注意,指向 ZST 的引用(包括空切片),就像所有其他引用一样,必须是非空的且适当对齐的。解引用指向 ZST 的空指针或未对齐的指针是未定义行为,就像任何其他类型一样。

空类型

Rust 还允许声明 *甚至无法实例化* 的类型。这些类型只能在类型级别上讨论,而不能在值级别上讨论。可以通过指定没有变体的枚举来声明空类型

#![allow(unused)]
fn main() {
enum Void {} // No variants = EMPTY
}

空类型比 ZST 更边缘化。空类型的主要动机示例是类型级别的不可达性。例如,假设一个 API 通常需要返回一个 Result,但一个特定的案例实际上是万无一失的。实际上,可以通过返回 Result<T, Void> 在类型级别上传达这一点。API 的使用者可以自信地解包这样的 Result,因为知道此值 *静态不可能* 是 Err,因为这需要提供类型为 Void 的值。

原则上,Rust 可以基于此事实进行一些有趣的分析和优化。例如,Result<T, Void> 仅表示为 T,因为 Err 情况实际上不存在(严格来说,这只是一种不保证的优化,因此例如将一个转换为另一个仍然是未定义行为)。

以下内容也可以编译

#![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* 来说效果相当好,并且可以将其转换为引用而没有任何安全问题。它仍然不能阻止你尝试读取或写入值,但至少它编译为无操作而不是未定义行为。

外部类型

有一个已接受的 RFC,用于添加具有未知大小的正确类型,称为 *外部类型*,这将使 Rust 开发人员能够更准确地对 C 的 void* 和其他“声明但从未定义”的类型进行建模。然而,截至 Rust 2018,该特性在 size_of_val::<MyExternType>() 的行为方式上仍处于停滞状态