Rust 类型中的奇行种

大部分情况下,我们考虑的都是拥有固定的正数尺寸的类型。但是,并非所有类型都是这样。

 

1. 动态尺寸类型(DST, Dynamically Sized Type)

Rust支持动态尺寸类型,即不能静态获取尺寸或对齐属性的类型。乍一看,这事有点荒谬——Rust必须知道一种类型的大小和对齐方式才能正确地使用它啊!从这一点来看,DST不是一个普通的类型。由于类型大小是未知的,只能通过某种指针来访问它。所以,一个指向DST的指针是一个“胖”指针,它包含指针本身和一些额外的信息(具体请往下看)。

语言提供了两种主要的DST:trait对象和slice。

trait对象表示实现了某种指定trait的类型。具体的类型被擦除了,取而代之的是运行期的一个虚函数表,表中包含了使用这种类型所有必要的信息。这就是trait对象的额外信息:一个指向虚函数表的指针。

slice简单来说是一个连续存储结构的视图——最典型的连续存储结构是数组或Vec。slice对应的额外信息就是它所指向元素的数量。

结构体可以在最后的位置上保存一个DST,但是这样结构体本身也就变成了一个DST。

// 不能直接存储在栈上
struct Foo {
    info: u32,
    data: [u8],
}

 

2. 零尺寸类型(ZST, Zero Sized Type)

Rust实际允许一种类型不占用内存空间:

struct Foo; // 没有成员 = 没有尺寸

// 所有成员都没有尺寸 = 没有尺寸
struct Baz {
    foo: Foo,
    qux: (),      // 空元组没有尺寸
    baz: [u8; 0], // 空数组没有尺寸
}

对于其自身来说,ZST显然没有任何用处。但是,和Rust中许多奇怪的布局选项一样,它的作用只在特定的上下文中才能体现:Rust认为所有产生或存储ZST的操作都可以被视为无操作(no-op)。首先,存储它没有什么意义——它又不占用空间。而且这种类型实际上只有一个值,所以加载它的操作可以凭空变一个值出来——而这种操作依然是no-op,因为产生的值不占用空间。

ZST的一个最极端的例子是Set和Map。已经有了类型Map<Key, Value>,那么要实现Set<Key, Value>的通常做法是简单封装一个Map<Key, UselessJunk>。很多语言不得不给UselessJunk分配空间,还要存储、加载它,然后再什么都不做直接丢弃它。编译器很难判断出这些行为实际是不必要的。

但是在Rust里,我们可以直接认为Set<Key> = Map<Key, ()>。Rust静态地知道所有加载和存储操作都毫无用处,也不会真的分配空间。结果就是,这段范型代码直接就是HashSet的一种实现,不需要HashMap对值做什么多余的处理。

安全代码不用关注ZST,但是非安全代码必须考虑零尺寸类型带来的影响。特别注意,计算指针的偏移量是no-op,标准的内存分配器(Rust默认使用jemalloc)在需要分配空间大小为0时可能返回nullptr,很难区分究竟是这种情况还是内存不足。

 

3. 空类型

Rust甚至也支持不能被实例化的类型。这种类型只有类型,而没有对应的值。空类型可以通过指定没有变量的枚举来声明它:

enum Void {} // 没有变量 = 空类型

空类型比ZST更加少见。一个主要的应用场景是在类型层面声明不可到达性(unreachability)。比如,假设一个API一般需要返回一个Result,但是在某个特殊场景下它是绝对不会出错的。这种情况在类型层面的处理方法是将返回值设为Result<T, Void>。因为不可能产生一个Void类型的值,所以返回值不可能是一个Err。知道了这一点,API的调用者就可以信心十足地使用unwrap

原则上来说,Rust可以基于这一点做一些很有意思的分析和优化。比如,Result<T, Void>可以表示成 T,因为实际上不存在返回Err的情况。下面的代码曾经也可以成功编译:

enum Void {}

let res: Result = Ok(0);

// 不存在Err的情况,所以Ok实际上永远都能匹配成功
let Ok(num) = res;

但是现在这些把戏已经不让玩了。所以Void唯一的用处就是明确地告诉你某些情况永远不会发生。

关于空类型的最后一个坑,创建指向空类型的裸指针实际上是合法的,但是对它解引用是一个未定义行为,因为这么做没有任何意义。也就是说,你可以使用*const Void模拟C语言的void *类型,但是使用*const ()却不会得到任何东西,因为这个函数对于随机解引用是安全的。

Rust允许你选择其他的数据布局策略。 1. repr(C)这是最重要的一种repr。它的目的很简单,就是和C保持一致。数据的顺序、大小、对齐方式都和你在C或C++中见到的一摸一样。所有你需要通过FF ...