原文链接:https://doc.rust-lang.org/nomicon/exotic-sizes.html

类型中的奇行种

在大多数情况下,我们希望类型具有静态已知的正数尺寸。在 Rust 中,情况并非总是如此。

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

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

语言提供了两种主要的DST:

  • trait对象:dyn MyTrait
  • slices:[T], str,以及其他

trait对象表示实现了某种指定trait的类型。具体的类型被擦除了,取而代之的是运行期的一个虚函数表,表中包含了使用这种类型所有必要的信息。使trait对象指针完整的信息是vtable指针。可以从vtable动态请求指针指向的对象的运行时大小。

slice简单来说是一个连续存储结构的视图——最典型的连续存储结构是数组或Vec。使slice指针完整的信息只是它指向的元素个数。指针指向的对象的运行时大小是元素的静态已知大小乘以元素个数。

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


#![allow(unused)]
fn main() {
// 不能直接存储在栈上
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目前基本上是半成品。)

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

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


#![allow(unused)]
fn main() {
struct Nothing; // 没有成员 = 没有尺寸

// 所有成员都没有尺寸 = 没有尺寸
struct LotsOfNothing {
    foo: Nothing,
    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-ops),分配器通常需要一个非零尺寸

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

空类型

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


#![allow(unused)]
fn main() {
enum Void {} // 没有变量 = 空类型
}

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

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

下面的代码也可以成功编译:

enum Void {}

let res: Result<u32, Void> = Ok(0);

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

但是现在这些把戏已经不行了。

关于空类型的最后一个需要注意的地方是,创建指向空类型的裸指针实际上是合法的,但是对它解引用是一个未定义行为,因为这么做没有任何意义。

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

*const ()(或等效变量)对于 void* 的效果相当好,并且可以作为应用而没有任何安全问题。它仍然不会阻止你尝试读取或写入值,但至少它会编译为无操作而不是未定义的行为。

外部类型(Extern Types)

公认的 RFC 可以添加大小未知的适当类型,称为外部类型,这将使 Rust 开发人员可以对C的void*和其他“声明但从未定义”的类型进行更准确的建模。但是从 Rust 2018 开始,该功能因 size_of::<MyExternType>() 的行为而陷入困境。