非常规大小类型

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

动态大小类型 (DST)

Rust 支持动态大小类型 (DST):大小或对齐方式在静态时未知的类型。从表面上看,这有点说不通:Rust 必须 知道某个东西的大小和对齐方式才能正确地使用它!在这方面,DST 不是普通类型。因为它们缺乏静态已知的大小,所以这些类型只能存在于指针之后。因此,任何指向 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 目前是一个很大程度上不完善的功能。)

零大小类型 (ZST)

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
}
}

就其本身而言,零大小类型 (ZST) 显然毫无用处。然而,与 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>() 的行为