CLASS 07 · 2026-02-25 · 抽象

抽象函数与表示不变量

本讲深入 ADT 的内部,学习两个核心概念:抽象函数(AF)把表示值映射到抽象值,表示不变量(RI)规定哪些表示值是合法的。它们是理解「不可变类型如何工作」的数学基础。

不变量

不变量(Invariant):在程序所有可能的运行时状态下都为真的性质。

不变量是 ADT 的「自我承诺」——无论使用者如何调用操作,ADT 始终维持这些性质。例如,一个表示日期的 ADT 应保证月份始终在 1 到 12 之间。

不可变性

不可变性是 6.102 中最重要的不变量之一。要保证不可变性,需要:

class ImmutablePerson {
  private readonly name: string;
  private readonly readonlyHobbies: ReadonlyArray<string>;

  constructor(name: string, hobbies: string[]) {
    this.name = name;
    // 防御性复制:保存一份不可变的快照
    this.readonlyHobbies = [...hobbies];
  }

  getHobbies(): ReadonlyArray<string> {
    return this.readonlyHobbies; // 返回只读视图,外部无法修改
  }
}

表示不变量与抽象函数

ADT 内部存在两个世界:

抽象函数(Abstraction Function)\(\text{AF}: R \to A\) 把每个表示值映射到它代表的抽象值。
表示不变量(Rep Invariant)\(\text{RI}: R \to \text{boolean}\) 判断一个表示值是否合法(well-formed)。

只有满足 RI 的表示值才是有效的,AF 只在这些有效值上有定义。

// 用一对数表示有理数
class Rational {
  private readonly numer: number; // 分子
  private readonly denom: number; // 分母

  // RI: denom > 0 且 gcd(|numer|, denom) == 1(已约分)
  // AF: AF(numer, denom) = numer / denom

  constructor(numer: number, denom: number) {
    if (denom === 0) throw new Error("denominator cannot be zero");
    const g = gcd(Math.abs(numer), denom);
    this.numer = numer / g;
    this.denom = denom / g;
  }
}

检查表示不变量

实现一个 checkRep() 方法,用断言检查 RI 是否成立。在每个操作(构造者、生产者、修改者)的末尾调用它,可以在开发阶段及早发现不变量被破坏的情况。

private checkRep(): void {
  assert(this.denom > 0);
  assert(gcd(Math.abs(this.numer), this.denom) === 1);
}

良性副作用

实现可以在内部修改表示值,只要修改后它仍然映射到同一个抽象值。这种修改称为良性副作用(benevolent side-effect)。例如,缓存一个计算结果不会改变抽象值,只是提升性能。

文档化 AF、RI 与表示泄露的安全性

ADT 的实现者应写下:

ADT 不变量替代前置条件

设计良好的 ADT 会自行封装和强制不变量,而不是把检查推给调用者。例如,Rational 的构造函数自动约分并检查分母非零,调用者无需自己保证这些条件。

如何建立不变量

  1. 在初始状态(构造函数)中使不变量为真。
  2. 确保所有改变状态的操作(生产者、修改者)都保持不变量为真。

对于不可变类型,只需检查第 1 步——因为对象一旦创建就不会改变。

核心要点

AF抽象函数 AF: R → A 把表示值映射到抽象值。

RI表示不变量 RI: R → boolean 判断表示值是否合法。

表示泄露通过别名暴露内部表示会破坏表示独立性和不变量。