类型系统:从编译期约束到运行时表征#
1. 类型不是一个东西,而是两套问题#
- 编译期:这个值允许做什么?(API 边界与正确性约束)
- 运行期:这份数据在内存里怎么摆?(内存布局、寻址方式与附加信息)
- 工程结论:工程师理解类型系统,不只是为了 “类型安全”,更是为了清楚抽象设计最终带来的性能成本(对象头、泛型实例化开销、GC 压力)。
2. 类型的物理表征:值、引用、对象与布局#
- 摒弃 “值在栈上、引用在堆上” 的错误认知,拆解为三个独立维度:
- 语义层面:值语义(赋值 / 传参复制数据) vs 引用语义(复制指针)。
- 存储位置:inline storage(数据直接嵌在宿主内) vs indirect storage(通过 reference / pointer 间接访问)。
- 内存管理:stack / heap / arena / static(生命周期由谁负责)。
- 底层落实:对象头、padding、alignment、字段布局。
- 对性能的直接影响:array/list/vector 的内存连续性与 CPU Cache Locality。
3. 对象模型:为什么 “一个对象” 并不便宜#
- Java/Kotlin/JVM:16 字节的空对象代价(HotSpot 64-bit 下的 Mark Word + Klass Pointer)。这是身份(Identity)、锁(Monitor)和 GC 的代价。提及 Project Lilliput 试图压缩头部的努力。
- Go 的 Interface:空接口(
eface: _type + data)与非空接口(iface: itab + data)的两套模型。
- Rust/C/C++:
- 普通 struct 大多降维为纯粹的 layout 和 offset,没有运行时统一的 type object。
- 动态派发时的代价:C++ 的侵入式 vtable,Rust 的胖指针(Fat Pointer)与 trait object。
- C#:
class vs struct 的物理差异,值类型如何零开销嵌入对象或数组中。
4. 装箱、拆箱与间接寻址#
- 本质:纯粹的 primitive/value 变成具有身份的 object/reference 时发生了什么。
- 成本拆解:allocation 开销、增加的对象头、pointer chasing(破坏预取)、引发更频繁的 GC。
- JIT 视角:通过 escape analysis 和 scalar replacement,什么时候能优化掉装箱,什么时候又无能为力。
- 实践警告:在热路径、大集合、高并发里频繁装箱为何危险。
5. 编译期类型语义#
- 静态类型 vs 动态类型。
- 强类型 vs 弱类型(澄清强/弱与静/动的正交关系)。
- 名义类型 (Nominal) vs 结构类型 (Structural)。
- 代数数据类型 (ADT):
- Product Type(
struct/class)。
- Sum Type(
enum),以及利用 Sum Type 进行 Nullable 替代、状态机建模的绝对优势(如 Result, Option)。
6. 多态:类型抽象如何落到机器上#
- Ad-hoc polymorphism(特设多态):重载、type class、trait bound。
- Subtype polymorphism(子类型多态):继承、接口、基于 vtable / itable 的运行时派发。
- Parametric polymorphism(参数多态):泛型的基本概念。
7. 泛型的运行时实现#
- Java:Type erasure(类型擦除)。共用同一份代码,但 primitive / value 支持受限。
- C++/Rust:Monomorphization(单态化)。性能极致但存在代码膨胀风险。
- Go:GC-shape stenciling + dictionary passing。在体积和性能之间取舍的混合路线。
- C#:Reified generics(具体化泛型)。运行时完整保留泛型信息并按需特化。
8. 回到工程判断#
- 什么时候该用 class/object,什么时候该用 struct/value type?
- 什么时候接口抽象值得做,什么时候会导致不必要的动态派发成本?
- 什么时候泛型会影响二进制体积或运行性能?
- 总结:类型的底层设计对内存布局、GC 压力、Cache Miss 以及 API 维护性的深远影响。