CLASS 09 · 2026-03-04 · 类型与函数式
等价性
相等性看似简单,实则暗藏陷阱。本讲学习等价关系的三条性质,区分引用相等与值相等,理解不可变类型与可变类型在相等性上的本质差异。
等价关系
相等性操作必须是一个等价关系(equivalence relation),满足三条性质:
- 自反性(Reflexive):\(a = a\)。
- 对称性(Symmetric):若 \(a = b\),则 \(b = a\)。
- 传递性(Transitive):若 \(a = b\) 且 \(b = c\),则 \(a = c\)。
任何相等性定义如果违反了这三条中的任何一条,就会产生微妙的 bug。
不可变类型的相等性
对于不可变类型,有两种定义相等性的方式:
- 使用抽象函数:两个实例相等当且仅当 \(\text{AF}(a) = \text{AF}(b)\)。
- 使用观察:两个对象相等当且仅当无法通过任何观察(调用观察者操作)区分它们。
对于不可变类型,这两种定义是等价的。
引用相等 vs. 值相等
- 引用相等(Reference equality):测试两个引用是否指向同一块存储。
- 值相等(Value equality):测试两个对象是否表示同一个值。
在 TypeScript 中,=== 是引用相等。对于对象,=== 只在两个引用指向同一对象时为 true。要实现值相等,需要自定义 equalValue() 方法。
class Rational {
constructor(
private readonly numer: number,
private readonly denom: number
) {}
equalValue(other: Rational): boolean {
return this.numer === other.numer && this.denom === other.denom;
}
}
const a = new Rational(1, 2);
const b = new Rational(1, 2);
const c = a;
console.log(a === b); // false:不同对象(引用不等)
console.log(a.equalValue(b)); // true:表示同一个值
console.log(a === c); // true:同一对象
破坏等价关系
定义相等性时必须小心维护等价关系的三条性质。例如,如果相等性依赖于可变状态,传递性就可能被破坏:对象 A 和 B 此刻相等,B 和 C 此刻相等,但 A 和 C 可能不等。
可变类型的相等性
可变类型有两种不同的相等性概念:
- 观察相等(Observational equality):此刻无法通过观察区分。
- 行为相等(Behavioral equality):此刻和未来都无法区分。
观察相等只关心当前状态;行为相等还要求未来所有操作都产生相同结果。对于可变类型,行为相等通常意味着它们是同一个对象(引用相等)。
集合上的「深度相等」
TypeScript 的 assert.deepStrictEqual() 为内置集合提供了观察相等:递归比较每个元素,而不要求是同一对象。
哈希函数
哈希函数必须与相等性一致:如果 a.equals(b) 为真,则 a.hashCode() === b.hashCode() 必须为真。否则,哈希表(Map/Set)会行为异常——相等的对象被放进不同的桶,导致查找失败。
核心要点
等价关系自反、对称、传递——相等性必须满足这三条。
引用 vs. 值=== 是引用相等;值相等需自定义。
可变类型观察相等(此刻)vs. 行为相等(永远)。