特殊尺寸类型

原文跟踪exotic-sizes.md   Commit: 7f019ec5c87da39fe0b9b5149e413d914528e945

大多数情况下,我们希望类型具有可静态确定的、非负的大小。 但在Rust中,并非总是如此。

动态大小类型(DST)

Rust支持动态大小类型(DST):无法静态地确定大小或对齐的类型。 从表面上看,这有点荒谬:Rust必须知道某些东西的大小和对齐单位才能正确使用它! 在这方面,DST不是正常的类型。 因为它们缺少静态可知的大小,所以这些类型只能存在于指针背后。 因此,任何指向DST的指针都会变成一个宽指针:由指针和"使其成为完整类型"的信息组成(下面将详细介绍)。

Rust语言暴露了两个主要的DST:

  • trait objects: dyn MyTrait
  • slices: [T], str, and others

trait对象表示实现了其指定的特质的某种类型。 原始类型通过一个包含了该类型所有信息的vtable被擦除,以支持运行时反射。 vtable指针让trait对象指针成为了一个完整类型:在运行时,可以从vtable中动态地获悉指针所指向的对象的大小。

切片只是一些连续内存的一个视图——通常是数组或Vec。 切片指针包含的额外信息只不过是它指向的元素的数量。 指针所指向对象的运行时大小只是单个元素的大小乘以元素的数量。

Struct实际上可以直接存储单个DST作为它们的最后一个字段,但这同时也让它们成为了DST:

// Can't be stored on the stack directly struct MySuperSlice { info: u32, data: [u8], }

尽管因为缺少构造手段,这种类型在很大程度上是没用的。 目前,唯一能够构造出自定义DST的方法是,在类型上增加泛型参数,并加以?Sized修饰:

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还允许声明不占用空间的类型:

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的任何操作都可以简化为no-op。首先,存储ZST甚至是没有意义的——它不占用任何空间。此外,ZST类型的值只有一个,所以任何需要用到它的时候,都可以凭空构造一个——这也是一种no-op,毕竟它不占用任何空间。

其中一个最极端的例子是SetMap。给定Map <Key,Value>,通常Set <Key>会被实现为Map <Key,UselessJunk>的一层薄薄的封装。在许多语言中,这需要为UselessJunk分配空间,并且需要储存和读取UselessJunk后再丢弃它。对编译器来说,证明这种不必要是很困难的。

但是在Rust中,我们可以说Set <Key> = Map <Key,()>。现在Rust编译器能够静态地得知每个读取和存储都是无用的,并且没有分配任何空间。结果就是,通过简单地单态化HashMap实现的HashSet,在HashMap的值上不需要花费任何额外开销。

safe的代码不必担心ZST,但在unsafe的代码中,必须小心ZST类型带来的后果。特别是,对指针偏移是no-op,并且当申请零大小的内存分配时,标准分配器可以返回null,因而无法区分这是否意味着出现了“内存不足”错误。

空类型

Rust甚至还允许声明无法实例化的类型。 这些类型只能在类型层面进行讨论,而不能在值的层面进行讨论。 可以通过指定不带变量的枚举来声明一个空类型:

enum Void {} // No variants = EMPTY

空类型比ZST更加边缘化。 空类型的主要积极示例是类型层面的不可达性。 例如,假设一个API通常需要返回一个结果,但实际上,这个API在特定情况是绝对可靠的。 这一点,通过返回一个Result <T,Void>,就可以在类型层面表达出来。 API的使用者可以自信地对这样的结果进行拆箱,因为已经知道这个值在静态上不可能是Err——否则就意味着需要提供一个Void类型的值,这是不可能的。

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

以下代码也可以通过编译:

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

但是这个trick现在还不能用。

关于空类型的最后一个细微的细节是,构造一个空类型的裸指针是合法的,但是解引用它们是未定义的行为,因为这没有意义。

我们建议不要使用* const Void对C的void *类型进行模拟。 很多人一开始会这样做,但很快就遇到了麻烦,因为Rust没有任何的安全措施来防止尝试用不安全的代码去实例化空类型,如果你这样做,那就是未定义行为。 这尤其成问题,因为开发人员习惯将原始指针转换为引用,而&Void也是未定义行为。

* const()(或等效的)适用于void *,并且可以在没有任何安全问题的情况下成为引用。 它仍然不会阻止您尝试读取或写入值,但至少它会编译为no-op而不是UB。

外部类型

有一个已经被接受的RFC为未知大小的类型增加了一种合适的类型,称为extern类型,这将使Rust开发人员更准确地模拟C的void *和其他"声明了但未定义"类型的内容。 但是,从Rust 2018开始,这个特性在size_of :: <MyExternType>()应该如何表现上陷入了困境。