数据类型系统
一、根本矛盾:物理世界没有类型,内存只有 0 和 1
要理解类型,必须先理解一个残酷的事实:
计算机硬件不知道"类型"是什么。内存里的每一位,物理上只是一个电荷状态——有电=1,没电=0。
一段 32 位的内存,长这样:
0100 0001 0110 0010 0011 0001 0101 1111
这玩意儿到底是什么?硬件回答不了。它可能是:
- 一个整数:1097692255
- 一个浮点数:3.14159(近似)
- 一个 UTF-8 字符串的一部分:"Ab1_"
- 一条 CPU 指令:add eax, ebx
- 一个内存地址:0x4162315F
- 一张图片的 4 个像素点
同一段二进制,用不同的"规则"去解读,含义完全不同。
类型,就是这个"解读规则"。
没有类型的世界(硬件):
二进制 → ??? → 谁也不知道它是什么
有类型的世界(编程语言):
二进制 + 类型(int) → 整数 1097692255
二进制 + 类型(float) → 浮点数 3.14159
二进制 + 类型(char[]) → 字符串 "Ab1_"
数据类型的根本动机:给无意义的二进制,赋予语义。
没有类型,程序就是瞎猜——不知道对这段二进制做加法和做字符串拼接,哪个是对的。
二、词源与本质定义
2.1 词源
Type 来自希腊语 typos(τύπος),本义是"刻下的印记、模型、模板"——就像铸造钱币用的模具,同一个模具出来的钱币都是同一个形状。
在计算机领域,引申为:一段内存的"模板"——它规定了这段内存能表示哪些值、以及能对它做哪些操作。
2.2 学术定义
学术界对"类型"有一个精确定义:
类型(Type)= 一个值的集合(Domain)+ 可以对这些值执行的操作集合(Operations)。
拆开讲:
2.3 值的集合(值域)
每个类型划定了"哪些值是合法的":
| 类型 | 值的集合 | 大小 |
|---|---|---|
boolean | {true, false} | 2 个 |
char(Java) | Unicode BMP 的所有字符 | 约 65536 个 |
int(Java) | [-2^31, 2^31-1] | 约 42 亿个 |
String | 所有可能的字符序列 | 无穷多个 |
User(自定义类) | 所有 User 类及其子类的实例 | 无穷多个 |
2.4 操作的集合
每个类型规定了"能对它做什么":
| 类型 | 允许的操作 | 不允许的操作 |
|---|---|---|
int | +、-、*、/、%、比较、位运算 | 字符串拼接、取长度 |
String | 拼接、取长度、取子串、比较 | 乘法、除法 |
boolean | &&、双竖线、! | 加法、乘法 |
关键洞察:
操作和值是绑定的。之所以不能对一个 boolean 做加法,不是因为"技术上做不到",而是因为语言的类型系统不允许——因为 boolean 值域里的 true/false,和"加法"这个操作在语义上不匹配。
类型系统就是一个静态的约束检查器:在程序运行之前,就把那些"语义上说不通"的操作拦住。
三、类型系统的两大分类轴
所有编程语言的类型系统,都落在这两个维度的组合上:
类型检查时机
│
静态类型 ────────────┬──────────── 动态类型
│
│
强类型 ─────────────┼────────────── 弱类型
│
类型强弱程度
3.1 第一轴:静态类型 vs 动态类型
静态类型(Static Typing):类型检查发生在编译期。程序跑之前,编译器就把每个变量的类型定死了。
// Java:静态类型
int age = 28; // 编译期就确定 age 是 int
age = "hello"; // 编译报错!不兼容的类型
代表语言:Java、C、C++、C#、Go、Rust、TypeScript
动态类型(Dynamic Typing):类型检查发生在运行期。变量本身没有类型,它的值才有类型。
# Python:动态类型
age = 28 # age 此刻指向一个 int
age = "hello" # 运行时 age 变成指向 str,没问题
age.length() # 运行时才检查 str 有没有 length()
代表语言:Python、JavaScript、Ruby、PHP、Lua
权衡对比:
| 维度 | 静态类型 | 动态类型 |
|---|---|---|
| 正确性 | 编译期抓错,更早发现问题 | 运行时才暴露 bug |
| 性能 | 编译期就知道大小和操作,生成更优机器码 | 运行时查类型,有额外开销 |
| 灵活性 | 改代码要重新编译 | 改代码直接跑,调试方便 |
| 可读性 | 类型是文档,看签名就知道传什么 | 全靠命名和注释 |
| 开发效率 | 前期写类型繁琐 | 前期写代码快 |
| 工具支持 | IDE 重构极准 | 重构容易漏 |
静态类型的核心优势:用编译期的约束,换运行时的确定性和性能。
动态类型的核心优势:用运行时的灵活,换开发前期的速度。
3.2 第二轴:强类型 vs 弱类型
强类型(Strong Typing):不允许隐式的类型转换(尤其是跨语义的转换)。类型之间的边界很"硬"。
# Python:强类型
1 + "2" # TypeError: unsupported operand type(s) for +: 'int' and 'str'
# int 和 str 不会偷偷互转,直接报错
代表语言:Java、Python、Rust、C#、Go
弱类型(Weak Typing):允许大量隐式转换,类型之间的边界很"软"。
// JavaScript:弱类型
1 + "2" // "12" —— int 转成 str,做拼接
1 + true // 2 —— true 转成 1,做加法
1 - "1" // 0 —— str 转成 int,做减法
代表语言:JavaScript、PHP、C(部分场景)
C 的定位很特殊:C 在算术运算上是弱类型(char 自动 int,int 自动 double),但在指针类型转换上又很强(需要显式 cast),所以 C 通常被归为中等偏弱。
3.3 四象限组合
注意:静态/动态 和 强/弱 是两个独立维度,不是一回事!
很多人混淆"静态=强"、"动态=弱",但看这张表就清楚了:
| 强类型 | 弱类型 | |
|---|---|---|
| 静态类型 | Java、C#、Go、Rust、TypeScript | C、C++(部分场景) |
| 动态类型 | Python、Ruby、Common Lisp | JavaScript、PHP |
典型例子:Python 是动态 + 强类型(运行时才检查,但检查很严格),TypeScript 是静态 + 强类型,JavaScript 是动态 + 弱类型。
四、类型的分类体系
讲完了分类维度,下面看具体有哪些类型。
4.1 所有语言共通的"类型金字塔"
类型系统
│
┌──────────────┼──────────────┐
│ │ │
基本类型 复合类型 特殊类型
│ │ │
┌─────┴─────┐ ┌────┴────┐ ┌────┴────┐
数值型 布尔/字符 数组/元组 函数/类 void/null
│ │ │ │
整型 浮点型 枚举/结构 对象/接口
char/bool
4.2 基本类型(Primitive Types)
不能再拆的"原子类型",直接由语言内核提供,通常有固定的内存大小和二进制表示。
| 子类 | 典型代表 | 共性 |
|---|---|---|
| 整数类型 | byte、short、int、long | 补码表示,有符号/无符号 |
| 浮点类型 | float、double | IEEE 754 标准,近似表示 |
| 字符类型 | char | UTF-16 / ASCII / Unicode |
| 布尔类型 | boolean / bool | true / false,1 位够但通常占 1 字节 |
基本类型的关键特征:
- 内存中直接存值,不是引用
- 通常在栈上分配(或寄存器),速度极快
- 没有方法(Java 的基本类型就是纯值,方法要靠包装类)
- 相等比较比较的是值本身(不是引用地址)
4.3 引用类型(Reference Types)
也叫复合类型/对象类型。由基本类型组合而成,可以无限扩展。
| 子类 | 典型代表 |
|---|---|
| 类(Class) | String、User、自定义类 |
| 接口(Interface) | List、Runnable |
| 数组(Array) | int[]、String[] |
| 枚举(Enum) | Season |
| 注解(Annotation) | @Override |
| 记录类(Record) | Java 16+ |
引用类型的关键特征:
- 内存中存的是地址(引用/指针),真正的对象在堆上
- 分配在堆上,由 GC 回收
- 有字段、有方法、有多态
- 相等比较(==)比较的是引用地址(除非重写 equals)
4.4 值类型 vs 引用类型:内存视角
这是理解类型系统最直观的方式。假设:
int a = 42;
String s = "hello";
内存里长这样:
栈(Stack) 堆(Heap)
┌─────────────────────┐ ┌─────────────────────┐
│ a: [00000000 00000000 │ │ │
│ 00000000 00101010]│ │ String 对象 │
│ │ │ ┌─────────────────┐ │
│ s: [0xAB12CD34]───────┼────────────→ │ char[] h e l l o │ │
│ │ │ │ hash=... │ │
└─────────────────────┘ │ └─────────────────┘ │
└─────────────────────┘
- 基本类型
a:栈上直接存了 4 个字节 = 值 42 - 引用类型
s:栈上存了 8 个字节(地址),真正的字符串内容在堆上
这就是为什么"Java 是值传递,但传进去的对象能被改掉"——传递的是引用的副本(值是地址),副本指向的是同一个堆对象。
五、类型系统的演进:五代发展路径
类型系统不是一成不变的,它经历了五代演进。每一代都是为了在"运行期正确性"和"开发期灵活性"之间找更好的平衡。
第一代 第二代 第三代 第四代 第五代
无类型 → 简单类型 → 泛型 → 类型推断 → 依赖类型
汇编 C/Fortran Java 5+ Kotlin/Swift Idris/Agda
机器码 Pascal C# 2+ TypeScript Coq
5.1 第一代:无类型(Untyped)
代表:汇编语言、机器码
没有类型概念,所有操作都是对裸字节的操作。程序员自己记着哪段内存是什么。完全灵活,也完全容易出错。
5.2 第二代:简单类型(Simple Types)
代表:C、Fortran、Pascal
有了基本类型(int、float、char)和复合类型(struct、array),但不能参数化。比如数组元素必须定死,不能写"任意类型的列表"。
5.3 第三代:泛型/参数多态(Generics / Parametric Polymorphism)
代表:Java 5+、C# 2+、C++ 模板
解决了"参数化类型"的问题:
// 没有泛型:每个类型写一个 List
class IntList { int get(int i) { ... } }
class StrList { String get(int i) { ... } }
// 有了泛型:一个 List 通吃
class List<T> { T get(int i) { ... } }
List<Integer> ints = new ArrayList<>();
List<String> strs = new ArrayList<>();
但 Java 的泛型有个臭名昭著的问题:类型擦除(Type Erasure)——编译后泛型信息就丢了,运行时 List<Integer> 和 List<String> 是同一个类。
// 编译后都变成 List<Object>,无法在运行时区分
new ArrayList<Integer>().getClass() == new ArrayList<String>().getClass() // true
类型擦除的原因:Java 5 引入泛型时,要求和 Java 1.4 的字节码 100% 兼容。这是典型的"历史包袱换方便"。C# 的泛型是运行时保留的,没有擦除问题。
5.4 第四代:类型推断(Type Inference)
代表:Kotlin、Swift、TypeScript、Rust、Java 10+ 的 var
解决了"泛型写起来太长、类型名写得太累"的问题:
// 以前
HashMap<String, List<User>> map = new HashMap<String, List<User>>();
// Java 7:钻石运算符
HashMap<String, List<User>> map = new HashMap<>();
// Java 10+:var 推断
var map = new HashMap<String, List<User>>();
注意:类型推断不是动态类型!类型还是静态的,编译器自动推断出来。var 推断出的类型一旦确定,再赋值其他类型还是会编译报错。
var age = 28; // 推断成 int
age = "hello"; // 编译报错:不兼容的类型
5.5 第五代:依赖类型(Dependent Types)
代表:Idris、Agda、Coq
类型可以依赖运行时值。比如可以定义"长度不超过 10 的数组"、"正数 int"、"两个参数相等等返回值"。
-- Idris:长度为 n 的列表,Vec 类型依赖 n 的值
data Vec : (n : Nat) -> (elem : Type) -> Type where
Nil : Vec 0 elem
(::) : elem -> Vec n elem -> Vec (n + 1) elem
依赖类型的终极目标:把程序的正确性证明,直接编码到类型系统里——类型检查通过 = 程序一定正确。目前主要在学术和验证领域,工业界尚未普及。
六、Java 类型系统全景
最后我们落到具体语言——Java 的类型系统长什么样?
6.1 Java 的类型图谱
java.lang.Object
│
┌───────────┬───────────┼───────────┬─────────────┐
│ │ │ │ │
Class Interface Array Enum Annotation
│
├── String
├── 包装类:Integer/Long/Double/Float/Boolean/Character/Byte/Short
└── 自定义类
(和上面完全独立的一支)基本类型:
boolean, byte, short, int, long, float, double, char
两个关键点:
- 基本类型不继承 Object——这就是为什么不能把
int直接塞进List(Java 5 之前要手动装箱) - 基本类型和包装类型之间的桥接 = 自动装箱/拆箱
6.2 自动装箱/拆箱
Integer boxed = 42; // 装箱:int → Integer(编译成 Integer.valueOf(42))
int unboxed = boxed; // 拆箱:Integer → int(编译成 boxed.intValue())
这是一层编译器做的糖,但代价不小:
- 拆箱时对象为 null → NPE
- 装箱产生大量短生命周期对象 → GC 压力
- == 比较:Integer.valueOf(127) == Integer.valueOf(127) 是 true(缓存),但 128 就是 false
包装类型的整数缓存(Integer Cache)是经典面试题:byte 范围内(-128~127)的值会被缓存,超过的每次都 new 新对象。所以包装类型永远用 equals 比较值,不要用 ==。
6.3 Java 类型系统的不足
| 不足 | 说明 |
|---|---|
| 基本类型的特殊性 | 8 个基本类型不参与泛型,导致包装类型的存在 |
| 泛型擦除 | 运行时丢失类型信息,带来反射、数组泛型等一系列问题 |
| 无值类型(Value Type) | Java 还在实现中(Project Valhalla),泛型 primitive 遥遥无期 |
| 无联合类型(Union Type) | TypeScript 的 string 或 number 联合类型,Java 做不到 |
6.4 为什么保留基本类型?
Java 同时存在基本类型和引用类型两套系统,这是类型系统"不纯粹"的直接表现。根本原因是一个经典的权衡:性能 vs 纯粹性。
如果 Java 全部使用引用类型(没有 int,只有 Integer),以 Integer 代替 int 做百万次循环,会发生什么?
// 全部用包装类型
Integer sum = 0;
for (Integer i = 0; i < 1000000; i++) {
sum += i;
}
每次循环都要发生拆箱、自增、装箱,百万次循环会创建约 200 万个临时 Integer 对象进堆,GC 压力剧增。
6.4.1 对象的三重物理代价
基本类型与包装类型在 int/Integer 上的量化对比:
| 维度 | int | Integer |
|---|---|---|
| 内存占用 | 4 字节 | 16 字节(对象头 12 + int 数据 4) |
| 数组开销 | 4N 字节 | 约 24N 字节(引用 8N + N 个对象各 16) |
| 创建开销 | 0(直接写栈) | new 对象 + 进堆 |
| GC 压力 | 0(栈自动回收) | 每个对象都是 GC 候选 |
| 访问开销 | 直接读值 | 两次内存访问(读引用 + 读对象) |
一个 Integer 对象的物理结构:
┌──────────────────────────┐
│ 对象头(12 字节) │
│ Mark Word (8B) │
│ 类指针 (4B) │
├──────────────────────────┤
│ int 值(4 字节) │
└──────────────────────────┘
总计:16 字节,75% 的空间不是数据本身
除了内存开销,还有两个关键差距:
- CPU 缓存友好性:
int[]内存连续,Integer[]每个对象分散在堆里,缓存命中率差 3-10 倍。 - GC 负担:基本类型在栈上,方法结束即回收,不触发 GC;包装类型进堆,百万级对象会显著增加 GC 扫描时间。
6.4.2 历史背景:1996 年的硬件限制
Java 1.0 发布于 1996 年,当时的硬件条件是:几十 MHz 的 CPU、几 MB 的内存、没有 JIT 编译器(HotSpot 1999 年才出现),也没有逃逸分析优化。
在那样的条件下,如果 int 是对象:
- 每个运算都要 new 对象,内存立即占满
- 没有高效 GC,频繁 Full GC 导致程序无法运行
基本类型是 Java 1.0 为了"在当时硬件上可用"做出的务实妥协:简单优先于纯粹,性能优先于优雅。
6.4.3 与 C# 统一类型系统的对比
C# 采用了统一类型系统:
System.Object
│
System.ValueType(值类型,栈上分配)
│ ├── int、double、struct
│
String、Class...(引用类型,堆上分配)
C# 的 int 是值类型,继承自 ValueType 再继承自 Object,既保持了基本类型的栈分配和值语义,又能在需要时当对象用(装箱才进堆)。
Java 没有这样做的原因:
| 原因 | 说明 |
|---|---|
| 简单性 | 基本类型和对象泾渭分明,不用理解值类型/引用类型的细微差别 |
| 性能 | 1996 年没有 JIT,值类型装箱拆箱的额外开销无法承担 |
| 设计周期 | Java 1.0 设计周期短,来不及做统一类型系统这种工程级改造 |
| C/C++ 血统 | 语法借鉴 C/C++,int/long 直接对应硬件原生类型,保持了映射关系 |
6.5 Integer Cache 与装箱拆箱陷阱
Java 5 引入自动装箱/拆箱,在两套类型之间架桥。但这层糖有几个容易踩的坑。
6.5.1 Integer Cache 的机制
Integer a = 127, b = 127;
System.out.println(a == b); // true
Integer c = 128, d = 128;
System.out.println(c == d); // false
原理来自 Integer.valueOf 的缓存逻辑:
// Integer.valueOf 源码(简化)
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
默认缓存范围是 -128 ~ 127,上限 high 可通过 JVM 参数 -XX:AutoBoxCacheMax=<size> 调整。
缓存目的是减少小整数频繁装箱产生的临时对象。这是包装类型性能差的一种补救,不是根本解决。
6.5.2 四类常见陷阱
陷阱 1:循环中大量隐式装箱
Integer sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += i; // 每次 += 都会触发:拆箱 sum → 自增 → 装箱新对象
}
// 100 万次循环创建约 100 万个临时 Integer 对象
陷阱 2:用 == 比较包装类型值
Integer x = 200, y = 200;
if (x == y) { ... } // false!超出缓存范围是两个不同对象
// 正确:x.equals(y)
陷阱 3:拆箱遇到 null 触发 NPE
Integer count = null;
int total = count; // NullPointerException!null 拆箱没有对应的值
陷阱 4:Map 与泛型中的隐式装箱
Map<Integer, String> map = new HashMap<>();
map.get(100); // 100 自动装箱成 Integer,每次多一次对象创建
// 高频场景下考虑用专门的集合:TIntObjectHashMap
结论:能用基本类型就不用包装类型。 只有泛型集合、需要 null 语义的场景才用。
6.6 现代 JVM 的补救:逃逸分析与标量替换
现代 JVM 的热点编译(C2)引入了逃逸分析,部分缓解了包装类型的性能问题。
6.6.1 什么是逃逸分析
分析对象的动态作用域,判断它是否"逃出"方法:
| 逃逸程度 | 说明 | 可做的优化 |
|---|---|---|
| 不逃逸 | 对象只在方法内使用 | 栈上分配 + 标量替换 |
| 方法逃逸 | 对象作为返回值或传参 | 有限优化(可能标量替换) |
| 线程逃逸 | 对象被其他线程访问(赋值给静态字段等) | 无法优化,正常堆分配 |
不逃逸的对象会被标量替换——把对象拆成几个基本类型字段,分别放到栈上,对象本身不复存在:
// 原始代码
public int calc() {
Point p = new Point(1, 2); // Point 不逃逸出方法
return p.x + p.y;
}
// JIT 标量替换后的等效代码
public int calc() {
int x = 1;
int y = 2;
return x + y;
}
标量替换配合锁消除,可以让"看起来在堆上"的对象实际上根本不进堆——这就是现代 JVM 里,写了很多小对象性能也不会崩的原因。
6.6.2 逃逸分析不能替代基本类型的原因
逃逸分析是"尽力而为"的优化,不能作为基本类型的替代:
- 不是保证:逃逸分析本身有开销,复杂代码路径可能分析失败,无法保证优化一定发生。
- 逃逸了就失效:对象一旦被放进集合、作为返回值、传给外部方法,优化立即失效。
- 性能确定性:高频热路径需要确定的性能,不能依赖 JIT 的"可能优化到"。基本类型提供 100% 确定的栈分配和零 GC 负担。
6.6.3 Project Valhalla:值类型的未来
Project Valhalla 是 Oracle 在推进的长期项目,目标是给 Java 引入值类型(Value Type):
// Valhalla 设想的语法(尚未正式发布)
value class Point implements Shape {
int x;
int y;
}
值类型的特性:
- 没有 identity(不能用 == 比较引用,不能 synchronized)
- 栈上分配,数组是连续内存(像 int[])
- 可以有字段、方法、实现接口(像普通类)
- 泛型可以直接用值类型,不再需要包装(List<int> 成为可能)
Valhalla 已经推进了十几年,涉及语言、字节码、JVM、核心库的全面改造,难度极大,截至 JDK 21 仍处于预览/孵化阶段。
6.6.4 实践中的选型原则
| 场景 | 用基本类型 | 用包装类型 |
|---|---|---|
| 局部变量 | 栈上分配,无 GC | 不必要 |
| 方法参数/返回值 | 避免装箱拆箱 | 需要 null 语义时 |
| 循环计数器 | 高频操作,必须用 | 禁止 |
| 数组元素 | 连续内存,缓存友好 | 不能 |
| 集合元素(泛型) | 擦除限制,不支持 | 只能用包装 |
| 数据库实体字段 | 可能未设置时 null 有语义 | 需要 null 时 |
| Map key/value | 擦除限制,不支持 | 只能用包装,高频场景考虑专用集合 |
选型原则:基本类型优先,包装类型是不得不做的妥协。
七、常见误解澄清
7.1 误解 1:"静态类型 = 写更多代码"
部分正确,但越来越不对。
类型推断(Kotlin、TypeScript、Java var)已经让静态类型的代码量接近动态类型。区别在于:静态类型的错误成本是编译时 1 秒,动态类型是运行时 1 小时。
7.2 误解 2:"弱类型 = 不好"
不对。弱类型有它的场景:JavaScript 处理网页 DOM 时,隐式转换反而更方便。问题不在于语言类型强弱,而在于用对了地方。弱类型适合小而灵活的脚本,强类型适合大型多人协作工程。
7.3 误解 3:"动态类型没有类型系统"
错误。动态类型也有类型系统,只是检查时机在运行时。Python、Ruby 都有非常严格的类型检查——只是不是在编译期。
真正"没有类型系统"的是汇编和机器码。
7.4 误解 4:"引用类型就是传引用"
经典混淆。Java 只有值传递,引用类型传的是"引用这个值",不是"传引用"(C++ 的 & 才是真的传引用)。
// Java:值传递,传的是"地址的副本"
void change(User u) {
u = new User(); // 改副本,外面没影响
}
// C++:传引用
void change(User& u) {
u = User(); // 外面真的被改了
}
7.5 误解 5:"类型系统只是约束,不能创造价值"
错误。现代类型系统(Rust、TypeScript)已经能表达极其丰富的语义:
- TypeScript 字面量联合类型:type Direction = "up" | "down" | "left" | "right"
- Rust 的所有权系统:通过类型系统保证内存安全,不用 GC
- Null 安全:String? vs String,把 NPE 消灭在编译期
类型系统已经从"防止写错"进化到"帮助写对"。
八、学习路径建议
第一阶段:建立概念
├── 理解"硬件没有类型"这个根本矛盾
├── 搞懂类型的本质定义(值的集合 + 操作的集合)
└── 写程序时思考:这段内存在被当作什么解读?
第二阶段:掌握两大分类轴
├── 静态类型 vs 动态类型(检查时机的区别)
├── 强类型 vs 弱类型(边界硬度的区别)
└── 理解四象限组合(Python=动态+强,JS=动态+弱)
第三阶段:深挖值类型 vs 引用类型
├── 栈上的值 vs 堆上的对象,内存布局差异
├── 值传递的真正含义(传引用 vs 传引用的值)
├── 自动装箱/拆箱与整数缓存陷阱
└── == vs equals 的正确用法
第四阶段:类型系统演进与工程实践
├── 泛型与类型擦除(Java vs C# 对比)
├── 类型推断(var、Kotlin、TypeScript)
├── Java 类型系统的不足与未来(Project Valhalla)
└── 针对业务场景选择合适的类型抽象(枚举 vs 常量类 vs 字典表)
九、总结
类型系统的本质,是给无意义的二进制赋予语义,并在编译期/运行时对这些语义做约束检查。
没有类型,程序就是一堆瞎猜的字节。类型的存在,让我们可以用"可验证的语义"来构建复杂系统。
类型系统的发展史,就是在"正确性"和"灵活性"之间不断找平衡点的历史:
- 静态类型 vs 动态类型:检查时机的选择
- 强类型 vs 弱类型:边界硬度的选择
- 基本类型 vs 引用类型:内存布局的区别
- 泛型、类型推断、依赖类型:不断向"既要正确又要灵活"逼近一句话记忆:类型是"解读二进制的规则",类型系统是"对规则的约束检查器"——类型越丰富,能表达的语义就越精确,能证明的正确性就越强。