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

repr(Rust)

首先,每种类型都有一个数据对齐属性(alignment)。一种类型的对齐属性决定了哪些内存地址可以合法地存储该类型的值。如果对齐属性是n,那么它的值的存储地址必须是n的倍数。所以,对齐属性2表示值只能存储在偶数地址里,1表示值可以存储在任何的地方。对齐属性最小为1,并且永远是2的整数次幂。

虽然不同平台的行为可能会不同,但基础类型通常都是按照它的类型大小对齐的。比如,在x86平台上u64f64都是按照4字节(32位)对齐的。

一种类型的大小都是它对齐属性的整数倍,这保证了这种类型的值在数组中的偏移量都是其类型尺寸的整数倍,可以按照偏移量进行索引。需要注意的是,动态尺寸类型的大小和对齐可能无法静态获取。

Rust有如下几种复合类型:

  • 结构体(带命名的复合类型 named product types)
  • 元组(匿名的复合类型 anonymous product types)
  • 数组(同类型数据集合 homogeneous product types)
  • 枚举(带命名的标签联合体 named sum types -- tagged unions)
  • 联合体(不带标签联合体,untagged unions)

如果枚举类型的变量没有关联数据,它就被称之为*无成员(field-less)*枚举。

默认情况下,复合结构的对齐属性等于它所有成员的对齐属性中最大的那个。Rust会在必要的位置填充空白数据,以保证每一个成员都正确地对齐,同时整个类型的尺寸是对齐属性的整数倍。例如:


#![allow(unused)]
fn main() {
struct A {
    a: u8,
    b: u32,
    c:u16,
}
}

在对齐属性与类型尺寸相同的平台上,这个目标会按照32位对齐。整个结构体的类型尺寸是32位的整数倍。它可能会转变成:


#![allow(unused)]
fn main() {
struct A {
    a: u8,
    _pad1: [u8; 3], // 为了对齐b
    b: u32,
    c: u16,
    _pad2: [u8; 2], // 保证整体类型尺寸是4的倍数
                    // (译注:原文就是“4的倍数”,但似乎“32的倍数”才对)
}
}

或者可能是:


#![allow(unused)]
fn main() {
struct A {
    b: u32,
    c: u16,
    a: u8,
    _pad: u8,
}
}

这里所有的类型都是直接存储在结构体中的,成员类型和结构体之间没有其他的中介。这一点和C是一样的。但是除了数组以外(数组的子类型总是按顺序紧密排列),其他的复合类型的数据分布规则并不一定是固定不变的。对于下面两个结构体定义:


#![allow(unused)]
fn main() {
struct A {
    a: i32,
    b: u64,
}

struct B {
    a: i32,
    b: u64,
}
}

Rust可以保证A的两个实例的数据布局是完全相同的。但是Rust目前不保证A的实例和B的实例有着一样的数据填充和成员顺序。

对于上面的A和B来说,这一点大概显得莫名其妙。可是当Rust要处理更复杂的数据布局问题时,它就变得很有必要了。

例如,对于这个结构体:


#![allow(unused)]
fn main() {
struct Foo<T, U> {
    count: u16,
    data1: T,
    data2: U,
}
}

现在考虑范型Foo<u32, u16>Foo<u16, u32>。如果Rust按照代码中指定的顺序布局结构体成员,那么它就必须填充数据以符合对齐规则。所以,如果Rust不改变成员顺序的话,他们实际上会变成这样:

struct Foo<u16, u32> {
    count: u16,
    data1: u16,
    data2: u32,
}

struct Foo<u32, u16> {
    count: u16,
    _pad1: u16,
    data1: u32,
    data2: u16,
    _pad2: u16,
}

后者显然太浪费内存了。内存优化原则要求不同的范型可以有不同的成员顺序。

枚举把这件事搞得更复杂了。举一个简单的枚举类型为例:


#![allow(unused)]
fn main() {
enum Foo {
    A(u32),
    B(u64),
    C(u8),
}
}

它的布局可能会是这样:


#![allow(unused)]
fn main() {
struct FooRepr {
    data: u64, // 根据tag的不同,这一项可以为u64,u32,或者u8
    tag: u8, // 0 = A, 1 = B, 2 = C
}
}

这也确实就是枚举的布局方式。

但是,在很多情况下这种表达方式并不是效率最高的。一个典型场景就是Rust的“null指针优化”:如果一个枚举类型只包含一个单值变量(比如None)和一个(潜在级联的)非null指针变量(比如Some(&T)),那么tag其实是不需要的。一个null指针完全可以用单值(unit)(None)变量来表示。所以,size_of::<Option<&T>>() == size_of::<&T>(),这个比较的结果是正确的。

Rust中的许多类型都包含或者本身就是非null指针,比如Box<T>Vec<T>String&T以及&mut T。同样的,你或许也能想到,对于级联的枚举类型,Rust会把多个tag变量合并为一个,因为它们本来就只有几个有限的可能取值。原则上,枚举类型可以使用相当复杂的算法来存储具有禁止值的整个嵌套类型中的二进制。因为这件事很重要,我们把枚举的问题留到后面讨论。