Rust 类型系统

一、内存布局

Rust 的类型系统强,首先是因为它死板。
Rust 不会帮你掩盖 CPU 对齐带来的麻烦。

同样的字节 0xBD,在 u8 里是 189,在 i8 里是 -67

对齐(Alignment)

CPU 不喜欢你在奇数地址上读 u64,Rust 编译器就帮你避开这些。

类型 对齐要求
u8 1 字节
u16 2 字节
u64 8 字节

结构体的对齐要求 = 字段里最大的那个。

#[repr(C)]
struct Foo {
    tiny: bool,
    normal: u32,
    small: u8,
    long: u64,
    short: u16,
}

repr(C) 下,它有一堆填充字节(padding),共 32 字节。
repr(Rust) 下,编译器可以重排字段顺序,可能只要 16 字节。

Rust 的规则很直接:要性能,就让编译器安排;要和 C ABI 对齐,就用 repr©。


二、特殊布局不是玩具

想自己干预内存布局?Rust 允许,但代价是你自己背锅
repr(packed)repr(align(n)) 是两个常见的“自找麻烦”的标签。


2.1 #[repr(packed)]:取消对齐

#[repr(packed)] 会强制结构体最小对齐为 1 字节(或你指定的 N),
也就是说字段可能落在未对齐的地址上。

Rust 的规定是:未对齐引用是未定义行为(UB)

错误示例:
#[repr(packed)]
struct P {
    a: u8,
    b: u32, // 可能未对齐
}

fn bad(p: &P) -> &u32 {
    &p.b // 产生未对齐引用,UB
}
正确姿势:
use core::ptr;

#[repr(packed)]
struct P { a: u8, b: u32 }

fn read_b(p: &P) -> u32 {
    unsafe { ptr::read_unaligned(&p.b as *const u32) }
}
什么时候该用 packed
  • 处理网络协议头、磁盘格式、外设寄存器
  • 绝不在普通业务逻辑或热路径中用

repr(packed) 是“内存拼图模式”:你想省几个字节,它可能让你付出 CPU pipeline stall 的代价。


2.2 #[repr(align(n))]:对齐放大器,不是装饰品

#[repr(align(n))] 提高类型的最小对齐要求。常见用途:

  1. 防伪共享(False Sharing):不同线程写不同字段时,避免共享同一个 cache line。
  2. SIMD/IO 加速:保证数据块 16/32/64B 对齐以便向量化。
示例:防伪共享
#[repr(align(64))]
struct AlignedU64(pub core::sync::atomic::AtomicU64);

struct Counters {
    a: AlignedU64,
    b: AlignedU64,
}

现在 ab 会在不同的 cache line 上,避免写入冲突。

示例:SIMD 对齐
#[repr(align(32))]
struct Block32([u8; 32]);

fn process(b: &Block32) {
    // 可以假设 b.0 是 32 字节对齐
}
不该乱用的情况:
  • 不知道 cache line 大小就瞎写 align(128)
  • 为了“看起来高级”乱贴。

三、Trait 对象安全(Object Safety)

Trait Object = 胖指针 (data_ptr, vtable_ptr)
编译器要能生成 vtable,就得提前知道每个方法的真实签名。

3.1 什么方法不能进 vtable?

1. 返回 Self 的方法
trait Builder {
    fn push(self, b: u8) -> Self;          // 不对象安全
    fn done(self) where Self: Sized;       // 限定在具体类型上用
}

Self 在不同类型上不一样,dyn Trait 根本不知道返回哪种类型。
解决办法是:where Self: Sized,告诉编译器“这方法只在具体类型上用”。


2. 带泛型参数的方法
trait Foo {
    fn map<T>(&self, f: fn(u8) -> T) -> T; // 泛型方法,不对象安全
    fn id(&self) -> u64;                   // 对象安全
}

dyn Foo 不可能提前知道 T 是什么类型。vtable 里不能放无限种版本。


3. 解决思路总结
问题方法 解决方法
返回 Self where Self: Sized 限定掉
泛型方法 拆出去、外移泛型、或改为关联类型
特殊接收者 仅支持 &self, &mut self, Box<Self>, Pin<&mut Self>

示例:保住对象安全
trait WriteBuf {
    fn write(&mut self, bytes: &[u8]) -> usize; // 可对象化
    fn into_inner(self) -> Vec<u8> where Self: Sized; // 只在具体类型可用
}

四、Trait Bound:类型逻辑的控制台

大多数人看到 where T: Trait 就像看到一串外星文字。
其实这是 Rust 类型系统的逻辑语句:“要想我编译,就满足这些关系。”


4.1 它是什么

where 子句用来声明约束关系。可以作用在:

  • 类型参数 (T: Trait)
  • 具体类型 (String: Clone)
  • 关联类型 (I::Item: Debug)
  • 生命周期 ('a: 'b)
  • 高阶生命周期(HRTB)
基础例子
fn dump<I>(it: I)
where
    I: IntoIterator,
    I::Item: core::fmt::Debug,
{
    for x in it { println!("{x:?}"); }
}
高阶生命周期(HRTB)
fn apply_all<F, T>(f: F, t: &T)
where
    F: for<'a> Fn(&'a T) -> &'a T,
{
    f(t);
}

4.2 常见错误与正确用法

错误 1:把约束绑在类型定义上
struct Bag<T: Debug> { items: Vec<T> } // 所有 T 都得 Debug

这会污染整个类型,复用性崩盘。

正确:

struct Bag<T> { items: Vec<T> }

impl<T> Bag<T> {
    fn push(&mut self, t: T) { self.items.push(t); }

    fn dump(&self)
    where
        T: Debug, // 只在需要打印时要求
    {
        for x in &self.items { println!("{x:?}"); }
    }
}

错误 2:用 dyn Trait 解决一切
fn process(xs: &mut dyn Iterator<Item = u8>) { /* ... */ } // 动态分发过度

动态分发适合插件点,不适合热路径。

正确:

fn process<I>(mut xs: I)
where
    I: Iterator<Item = u8>, // 静态分发,零成本
{
    while let Some(b) = xs.next() { /* ... */ }
}

错误 3:泛型方法杀死对象安全
trait Transform {
    fn map<T>(&self, f: fn(u8) -> T) -> T; // 
}

正确:

trait Producer {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

4.3 高级用法与技巧

(1) 关联类型:让约束更干净
trait Storage {
    type Key: Ord + Clone;
    type Val: Clone;

    fn get(&self, k: &Self::Key) -> Option<Self::Val>;
}
(2) 高阶生命周期(HRTB)
fn stable_ref<'t, F, T>(t: &'t T, f: F) -> &'t T
where
    F: for<'a> Fn(&'a T) -> &'a T,
{
    f(t)
}
(3) ?Sized 接受切片与 Trait 对象
fn len<T: ?Sized>(t: &T) -> usize
where
    for<'a> &'a T: IntoIterator,
{
    t.into_iter().count()
}
(4) 类型逻辑表达式
fn debug_any_iter<T>(t: &T)
where
    for<'a> &'a T: IntoIterator,
    for<'a> <&'a T as IntoIterator>::Item: Debug,
{
    for x in t { println!("{x:?}"); }
}
(5) 最小约束原则
fn default_of<T: Default>() -> T { T::default() } // 

where 是 Rust 抽象的安全网。写太多约束是懒惰,写太少约束是模糊。


五、impl Trait 与存在类型:隐藏,不是逃避

impl Trait 是存在类型。
它告诉调用者:“我返回一个满足 Trait 的类型,但你不需要知道是谁。”

fn evens() -> impl Iterator<Item = i32> {
    (0..).filter(|x| x % 2 == 0)
}
  • 编译期仍是静态分发(零开销)
  • 调用者只看到抽象接口
  • API 稳定、类型干净

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐