类型系统
类型是对程序行为的静态近似:不运行程序,就能排除一部分错误。本章讲类型检查与推断(Hindley–Milner)、多态与子类型、代数数据类型,以及类型系统能防止什么、不能防止什么——最后用 Rust 的所有权类型看现代的边界(→ 语法与语义)。
类型系统的光谱
Section titled “类型系统的光谱”两组常常混谈的维度要分开:
| 维度 | 含义 | 例子 |
|---|---|---|
| 静态 vs 动态 | 类型检查发生在编译时还是运行时 | Rust/Java vs Python/JS |
| 强 vs 弱 | 类型转换是否隐式大量发生 | Python vs JavaScript |
"强/弱类型"没有严格定义,讨论时应说明具体含义(隐式转换多少、指针可否随意重解释等)。C 是静态但弱的(指针可以强制转换);Python 是动态但强的("1" + 1 报错)。
静态检查发生在编译时,动态检查发生在运行时——静态类型语言把一类错误提前到编译期,代价是失去部分灵活性(或需要更复杂的类型系统来表达)。现代的趋势是混合:动态语言加类型注解(Python typing、TypeScript),静态语言加强推断。
Hindley–Milner 算法让静态类型语言不需要写类型注解:从表达式局部推断出全局类型(Algorithm W)。ML 家族(OCaml、Haskell)与 Rust 的局部推断都源于此。类型推断让静态类型语言写起来像动态语言——推断是全局的(HM)或局部的(Rust:函数签名要写,函数体不用)。
- 参数多态:同一代码对多个类型工作(泛型、Haskell 的类型变量)。实现方式:编译期实例化(C++ 模板、Rust 单态化)或类型擦除 + 字典传递(Java 泛型、GHC)。
- 子类型多态:子类出现在父类使用的位置(面向对象,→ 编程范式)。
- 特设多态:重载与类型类(接口)。
两种多态的组合(泛型 + 子类型,如 Java 的通配符)是类型系统复杂度的著名来源(variance 协变逆变)。
代数数据类型
Section titled “代数数据类型”和类型(sum,"或")与积类型(product,"且")的组合:
enum Shape { // 和类型:三选一 Circle { radius: f64 }, Rect { w: f64, h: f64 },}
fn area(s: &Shape) -> f64 { match s { // 穷尽性检查:漏一个分支编译不过 Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rect { w, h } => w * h, }}Option/Result(可空与错误)是和类型:"可能没有"成为类型的一部分,空指针一类错误在编译期被排除。穷尽性检查(match 必须覆盖所有构造)让"加一个枚举值"变成编译器强制的全局检查。
类型系统能防什么、不能防什么
Section titled “类型系统能防什么、不能防什么”能防:类型错误、空引用(Option)、数据竞争(Rust/Send+Sync)、资源泄漏(RAII/所有权)。不能防:逻辑错误(把半径当直径)、死锁(类型正确的死锁)、越界(多数语言的数组仍运行时检查)。
类型系统防的是整类错误,以表达力为代价:类型系统越强(依赖类型、线性类型),表达得越多,学习与工程成本越高。
Rust 的所有权:类型化的内存安全
Section titled “Rust 的所有权:类型化的内存安全”Rust 把内存管理放进类型系统(→ 运行时、内存管理与垃圾回收):
- 所有权:每个值有唯一 owner,离开作用域即释放——无 GC 的确定性析构;
- 借用:任意多个不可变借用
&T或一个可变借用&mut T——别名与可变不同时存在; - 生命周期:引用的有效范围在编译期检查。
所有权与借用把内存安全交给类型检查:释放后使用、数据竞争、悬垂指针在编译期被排除,运行时零开销(→ 内存安全与常见漏洞)。代价是与借用检查器搏斗的学习曲线。
- 类型是对程序行为的静态近似;静态/动态与强/弱是两个维度,"强弱"没有严格定义
- Hindley–Milner 推断让静态语言写起来像动态语言
- 参数多态的两种实现(单态化 vs 擦除)决定运行时行为
- 代数数据类型 + 穷尽性检查把"可能没有"与"新增枚举"变成编译期问题
- Rust 的所有权与借用把内存安全交给类型检查,零运行时开销