从C到Rust:最基础的Trait Sized

10 阅读10分钟

Sized —— 数据的大小

一、这个 Trait 定义了数据的什么行为

Sized 是 Rust 中最基础的 Trait——它甚至不需要你手动实现:


// Sized trait 的定义(实际上在 std::marker 中)

pub trait Sized { } // 自动实现,不需要 impl

Sized 的含义:"这个类型在编译期有已知的固定大小。"


use std::mem::size_of;

  


// 这些类型都是 Sized——编译器知道它们有多大

size_of::<i32>(); // 4 字节

size_of::<f64>(); // 8 字节

size_of::<[i32; 10]>(); // 40 字节(4 * 10)

size_of::<(i32, f64)>(); // 16 字节(4 + 8 + padding)

  


// 这些类型不是 Sized——编译器不知道它们有多大

// size_of::<[i32]>(); // ❌ 编译错误:未确定大小的切片

// size_of::<str>(); // ❌ 编译错误:未确定大小的字符串

// size_of::<dyn Debug>(); // ❌ 编译错误:未确定大小的 trait 对象

1.1 Sized 的隐式约束

Sized 是所有泛型参数的默认约束


// 这两个签名等价:

fn example<T>(x: &T) {}

fn example<T: Sized>(x: &T) {} // T: Sized 是隐式的

要接受动态大小类型(DST),必须显式写出 ?Sized


// 接受任意类型,包括 DST

fn example<T: ?Sized>(x: &T) {}

// ↑ "T 可能不是 Sized"

1.2 为什么几乎所有类型都是 Sized

Rust 编译器需要知道每个类型的大小,以便:

  • 在栈上分配空间(每个函数帧的大小是编译期确定的)

  • 计算结构体的字段偏移

  • 确定数组的步长

  • 生成函数调用约定(参数和返回值的大小)


// 编译器需要知道 T 的大小来分配栈空间

fn process<T>(x: T) { // T: Sized 是默认的

// 在栈上为 x 分配 sizeof::<T>() 字节

}


二、C 中是什么

2.1 C 中一切都是 Sized

在 C 中,每个类型都有编译期已知的大小——sizeof(T)


#include <stddef.h>

  


sizeof(int); // 4(通常)

sizeof(double); // 8

sizeof(struct Point); // struct 的大小(含 padding)

sizeof(int[10]); // 40(4 * 10)

C 中没有"编译期不知道大小"的类型。每个类型都被视为有确定大小。

2.2 C 中唯一的例外:灵活数组成员

C99 引入了灵活数组成员(flexible array member),它允许结构体的最后一个字段是未指定大小的数组:


// 灵活数组成员——大小不完全确定

struct FlexArray {

int len;

char data[]; // 灵活数组成员:大小在运行时决定

};

  


// sizeof 不包含 data 的大小:

sizeof(struct FlexArray); // = sizeof(int)(可能 + padding)

// 实际分配时需要手动计算:

struct FlexArray* fa = malloc(sizeof(struct FlexArray) + 100);

fa->len = 100;

fa->data[0] = 'a'; // 手动管理——不知道是否越界

C 对灵活数组成员的处理:

  • sizeof 不包括数组成员的大小

  • 编译器不会检查 data[i] 是否越界

  • 程序员必须手动计算和分配额外空间

  • 没有类型系统来表达"这是一个不确定大小的类型"

2.3 C 中数组退化为指针


// C 中数组参数退化为指针——丢失大小信息

void process(int arr[]) {

// arr 是 int*,不是 int[10]

// sizeof(arr) == sizeof(int*) // 8(64 位),不是 40!

}

C 中数组作为函数参数时"变成"了指针——这个"丢失大小"就是 C 没有 ?Sized 概念的后果。C 把数组当做 Sized,但传参时悄悄把它变成了指针。

2.4 sizeof 在 C 中的局限


// sizeof 不能用于灵活数组成员

struct FlexArray* fa = /* ... */;

// sizeof(fa->data) // ❌ 编译错误:对不完全类型使用 sizeof

  


// void 是不完全类型

// sizeof(void) // ❌ 编译错误:对不完全类型使用 sizeof

  


// 函数类型不能 sizeof

// sizeof(void()) // ❌ 编译错误

C 的 "不完全类型"(incomplete type)是唯一接近 ?Sized 的概念,但它不能出现在函数参数中、不能作为变量的类型、不能在泛型上下文中使用(C 也没有泛型)。


三、C 的问题

3.1 没有"动态大小类型"的概念

C 不能表达"这个类型的大小在编译期不确定":


// C 中无法安全地传递一个"不确定大小"的数组视图

// 只能用 int* + size_t

void process(int* data, size_t len);

// int* 丢失了"这是数组还是单个 int"的信息

// len 的准确性全靠调用者保证

3.2 灵活数组成员不安全


struct FlexArray {

int len;

char data[];

};

  


struct FlexArray* create(int n) {

struct FlexArray* fa = malloc(sizeof(struct FlexArray) + n);

fa->len = n;

return fa;

}

  


// 问题 1:无法在栈上分配

// struct FlexArray fa; // ❌ 编译器不知道大小

  


// 问题 2:不能作为数组元素

// struct FlexArray arr[10]; // ❌ 不能

  


// 问题 3:复制时不会复制 data

// struct FlexArray fa2 = *fa; // ❌ 只复制了 len,没有复制 data

  


// 问题 4:越界无保护

fa->data[1000] = 'x'; // ✅ 编译通过,运行时可能越界

3.3 数组退化为指针丢失大小


void process(int arr[10]) {

// 10 被忽略了——arr 是 int*

// 无法在这里检查访问是否越界

for (int i = 0; i < 10; i++) { // 只能靠程序员记住"约定"的 10

arr[i] = 0;

}

}

C 无法在原地保留数组的大小信息——传参就丢失。

3.4 C 没有统一的泛型约束来表达"大小不确定"

C 没有泛型,因此也就没有"对一个可能不确定大小的类型做操作"的需求。但也正因如此,C 缺少所有建立在 ?Sized 上的抽象能力:

  • 没有切片类型(&[T]

  • 没有字符串切片(&str

  • 没有 Trait 对象(dyn Trait


四、Rust 为什么需要这个 Trait

4.1 Sized 是类型系统的基础设施

Sized 自动实现,不需要程序员干预。它的核心作用是作为泛型的默认约束


// 默认:T: Sized

struct Container<T> { // T 默认是 Sized

data: T, // 编译器需要知道 T 的大小来布局结构体

}

  


// 显式:T: ?Sized —— 允许 DST

struct Container<T: ?Sized> {

data: Box<T>, // T 可能不是 Sized,所以用 Box

}

4.2 Sized 与栈分配


// 栈分配要求类型是 Sized

let x: i32 = 42; // ✅ i32 是 Sized——在栈上分配 4 字节

let y: [i32; 5] = [0; 5]; // ✅ [i32; 5] 是 Sized——20 字节

  


// 但:

// let z: [i32] = [1, 2, 3]; // ❌ 编译错误![i32] 不是 Sized

// [i32] 没有编译期已知的大小——不能直接在栈上分配

// 必须用切片引用:&[i32](胖指针,16 字节,是 Sized 的)

4.3 动态大小类型(DST)必须放在指针后面


use std::mem::size_of;

  


// DST 类型本身不是 Sized:

// [i32] —— 切片(长度不定)

// str —— 字符串(字节数不定)

// dyn Trait —— Trait 对象(具体类型不定)

  


// 但 DST 的胖指针引用是 Sized 的:

size_of::<&[i32]>(); // 16(ptr + len)

size_of::<Box<[i32]>>(); // 16(ptr + len)

size_of::<&str>(); // 16(ptr + len)

size_of::<Box<dyn Debug>>(); // 16(ptr + vtable)

size_of::<Rc<dyn Trait>>(); // 16(ptr + vtable)

这就是 Sized 的核心意义:它让编译器知道什么时候可以在栈上分配,什么时候必须通过指针间接使用。


Sized 类型 → 可以直接使用(栈分配、作为结构体字段内联)

?Sized 类型 → 必须通过指针使用(&、Box、Rc 等)

4.4 Sized 与 ?Sized 的边界

?Sized 约束主要在泛型函数中使用,以便同时接受 Sized 和 DST:


// 只接受 Sized 类型

fn only_sized<T>(val: T) { // T: Sized 是默认的

// T 在栈上

}

  


// 接受所有类型(包括 DST)

fn accept_dst<T: ?Sized>(val: &T) {

// T 可能不是 Sized,所以通过引用传递

}

  


// 标准库中的典型例子:

// impl<T: ?Sized> Clone for Box<T> { ... }

// Box 可以对任何类型实现 Clone——包括 [i32] 和 dyn Trait

// 因为 Box 本身是 Sized 的(胖指针),不管 T 是不是 Sized

4.5 标准库中的 ?Sized 使用


// Box<T: ?Sized> —— Box 可以装 DST

let box_slice: Box<[i32]> = Box::new([1, 2, 3]); // [i32] 是 DST

let box_str: Box<str> = Box::from("hello"); // str 是 DST

let box_dyn: Box<dyn Debug> = Box::new(42); // dyn Debug 是 DST

  


// 切片引用就是胖指针——知道长度

let slice: &[i32] = &[1, 2, 3, 4];

// 在 64 位系统上:16 字节(ptr + len)

  


// Trait 对象也是胖指针——知道 vtable

let debug: &dyn Debug = &42;

// 在 64 位系统上:16 字节(ptr + vtable)

4.6 Sized 与 C 的 sizeof 对比

维度C sizeofRust Sized
是一个操作符sizeof(T)std::mem::size_of::<T>() 是函数
对 DST 的行为❌ 灵活数组成员不包含在 sizeof 中❌ DST 不能调用 size_of
作为类型约束❌ 不能表达"T 必须有大小"T: Sized 默认约束
作为泛型边界❌ 没有泛型T: ?Sized 显式放宽
运行时检查❌ 无法检测✅ 通过 DST 胖指针携带长度

4.7 一个实际的例子:为什么需要 ?Sized


// 想写一个可以接受"任意类型的引用"的函数

  


// ❌ 错误:T 默认是 Sized

fn print_it<T: Debug>(val: &T) {

println!("{:?}", val);

}

print_it(&[1, 2, 3]); // ✅ &[i32; 3] 是 &T,T 是 [i32; 3](Sized)

// print_it(&[1, 2, 3] as &[i32]); // ❌ &[i32] 中 [i32] 不是 Sized

  


// ✅ 正确:T: ?Sized

fn print_it2<T: ?Sized + Debug>(val: &T) {

println!("{:?}", val);

}

print_it2(&[1, 2, 3] as &[i32]); // ✅ [i32] 是 ?Sized

print_it2("hello world"); // ✅ str 是 ?Sized


五、与 C 程序员的对话

"C 中所有类型都有 sizeof,Rust 不也一样?"

C 程序员:"C 中所有类型都可以 sizeof,Rust 的 Sized 不是一样的东西吗?"

Rust:"运行时效果一样——两者都知道类型的大小。但区别在于类型系统:C 中所有类型都是 Sized(除了灵活数组成员的一些特殊处理)。Rust 明确区分了 Sized 和 ?Sized——有些类型([i32]strdyn Trait)不是 Sized。这种区分延伸到泛型约束中——你可以写 T: ?Sized 来表达'这个泛型参数可能不是一个有固定大小的类型'。C 没有这种能力。"


// C —— 所有类型都是"有固定大小的"

int x; // ✅ 知道大小

int arr[10]; // ✅ 知道大小

int[] /* 参数 */; // ❌ 退化为指针——但这是传参的特殊规则,不是类型系统的特性


// Rust —— 显式区分

let x: i32; // ✅ Sized

let arr: [i32; 10]; // ✅ Sized

let slice: &[i32]; // ✅ &[i32] 是 Sized(胖指针)

// [i32] // ❌ 不是 Sized——不能直接使用

"那 ?Sized 有什么用?"

C 程序员:"我写代码从来没遇到过需要不确定大小的类型,?Sized 在什么场景用?"

Rust:"写泛型代码时用到——尤其是标准库作者。比如 Box<T: ?Sized> 让 Box 可以装 [i32]strdyn Trait。如果你只写应用代码,确实很少直接接触 ?Sized。但如果你用过 &[i32]Box<dyn Trait>Rc<str>——这些就依赖于 ?Sized 的存在。"


// 标准库中 ?Sized 的使用

impl<T: ?Sized> Clone for Box<T> { /* ... */ }

impl<T: ?Sized + PartialEq> PartialEq for Rc<T> { /* ... */ }

impl<T: ?Sized> Deref for Box<T> { /* ... */ }

  


// 如果你自己写容器,可能需要:

pub struct MyBox<T: ?Sized> {

ptr: *mut T,

}


六、小结

6.1 Sized 在 Rust 类型系统中的地位


Sized(默认约束)

│

├── 所有基本类型:i32, f64, bool, char ...

├── 所有 struct/enum(除非内部有 ?Sized 字段)

├── 所有引用 &T、&mut T(普通引用是 Sized 的)

└── 所有胖指针 &[T]、Box<dyn Trait> ...

│

└── 它们指向的 [T]、strdyn Trait 是 ?Sized

Sized 是 Rust 类型系统中"看不见但处处都在"的基础设施。 它是所有泛型参数的默认约束,是栈分配的前提条件,是堆分配与指针间接使用的分界线。

6.2 C 没有的概念

Rust 的概念C 中有没有为什么
Sized — 标记类型有固定大小有(所有类型隐含)但 C 无法在类型系统中表达
?Sized — 标记类型不一定有固定大小没有C 没有 DST 的概念
[T] — 动态大小切片没有数组退化为指针
str — 动态大小字符串没有C 用 char* + NULL 结尾
dyn Trait — 动态大小 trait 对象没有C 没有 trait

6.3 一句话总结

C 中所有类型都是"固定大小"的,语言没有"不确定大小的类型"这个概念——灵活数组成员是唯一的例外,但它的处理方式不安全(无边界检查、无类型系统表达)。Rust 的 Sized Trait 明确区分了"固定大小"和"不确定大小":Sized 类型可以直接使用(栈分配、字段内联),?Sized 类型必须通过指针间接使用。这个区分让 Rust 可以安全地表达切片、字符串切片、Trait 对象等动态大小类型,同时保持编译器的类型检查。