PAYATHON 2026

OOP 中是否存在与 extension 相反的 class reduction?

技术老齐

结论

这个设想可以实现,也有相近的语言机制和理论研究可供参考。设计时,需要分别处理“复用已有实现”和“成为原类的子类型”这两件事。

Java 不支持题设中的 class C reduces B。已有机制能满足其中一部分需求:

  • trait 的 exclusion 操作允许在组合实现时排除方法。
  • Ruby 的 undef_method 可以阻断继承方法的查找,同时保留继承关系。
  • 继承与子类型分离的研究,为“复用一个类,却不承诺兼容该类”提供了理论基础。

这些机制的语义不同,“可以删除方法”并不意味着完整支持题设中的 reduction。

为什么删除成员会影响子类型关系

假设一段代码接收 B:

void useB(B value) {
    value.methodB();
}

如果 C 能作为 B 传入,却没有 methodB(),这段代码就无法按预期调用该方法。因此,真正移除 methodB() 后,就不能再无条件地将 C 视为 B 的子类型。

“减少约束”的直觉有道理:要求越少,能够满足要求的对象通常越多。不过,这说的是类型约束的放宽,不能据此认定成员更少的类可以替代原类。

语言可以分别定义两种关系:

  • 实现复用关系:C 的部分代码来自 B。
  • 类型兼容关系:C 的对象可以安全地用于要求某个类型的位置。

拥有相同方法,不等于 Java 中的同一类型

即使 C 拥有与 A 相同的 public 方法,Java 也不会因此把它视为 A。Java 的类和接口采用名义类型规则,兼容性取决于声明的继承、实现关系;instanceof 不会搜索“API 最接近的祖先”。参见 JLS §15.20.2。

结构类型系统可以根据成员及其类型判断兼容性,但方法同名还不够,还需要考虑参数类型、返回类型等规则。如果还要求行为兼容,就要考虑方法的前置条件、后置条件和对象不变量。

所以,“a 不是 public,删除后仍兼容 A”也需要额外前提。外部代码无法直接访问私有字段,保留的方法却可能依赖它:

class A {
    private int a;

    public void methodA() {
        a++;
    }
}

删除 a 后,必须相应调整 methodA() 的实现,私有访问权限并不会消除方法对字段的依赖。

已有语言机制能做到什么

trait exclusion:在组合前排除实现

trait 将可复用的方法组织成独立的组成部分。支持 exclusion 的 trait 系统允许先从一个 trait 中排除方法,再让它参与其他组合。这与“保留大部分实现,只修改少量功能”的需求很接近。

经典研究 Traits: Composable Units of Behaviour 讨论了组合、排除和别名,并有基于 Squeak 的实现。经典模型中的 trait 不直接定义实例字段,主要解决方法复用问题。参见研究项目说明和论文技术报告。

exclusion 的具体语义需要逐一确认。排除某个方法的实现后,剩余方法可能仍然调用它,需要其他组成部分补上实现。

Racket 提供了这些操作:

(trait-exclude some-trait methodB)
(trait-exclude-field some-trait a)

其中,trait-exclude 通常会把被排除的方法定义替换成继承声明;trait-exclude-field 则会添加 inherit-field 声明。它们移除了当前 trait 提供的定义,但仍可能要求最终的基类或组合提供对应成员。因此,不能将这两条表达式直接理解为“最终对象不再拥有该成员”。参见 Racket Traits 文档。

Ruby:让子类不再响应继承方法

Ruby 的 undef_method 可以直接阻断继承方法的查找:

class B
  def methodA
    :kept
  end

  def methodB
    :removed
  end
end

class C < B
  undef_method :methodB
end

c = C.new

c.methodA                 # => :kept
c.respond_to?(:methodB)    # => false
c.is_a?(B)                # => true

# c.methodB               # raises NoMethodError

这样可以保留其他方法,同时取消某个继承方法。不过,C 仍然继承自 B,undef_method 也不提供静态依赖检查或字段移除保证。

remove_method 的行为有所不同:移除当前类自己的方法定义后,仍可沿祖先链寻找同名方法;undef_method 则会阻断这条查找路径。参见 Ruby Module#undef_method。

有哪些相关理论研究

可以从 Cook、Hill 和 Canning 的 Inheritance Is Not Subtyping 看起。该研究讨论了通过继承增量构造实现时,得到的对象不一定与原对象具有子类型兼容关系。它支持将实现复用与子类型兼容分别建模,但没有规定题设中的 reduces 语法或完整的删除规则。参见论文原文。

另一个相关方向是 Gilad Bracha 的研究语言 Jigsaw。它通过模块操作表达组合和修改,其中 restrict 可以删除成员的定义、保留声明,让它成为待满足的要求。这接近“对已有定义做减法”,但内部引用仍会带来限制:删除实现与彻底删除成员是两种操作。参见 The Programming Language Jigsaw。

继续查资料时,可以使用这些术语:

  • inheritance without subtyping
  • trait exclusion
  • module composition
  • restriction、projection

class reduction 可以用来描述这个设想,但不同资料中的含义未必相同。

如果要设计完整的 reduction,需要哪些规则

题设中的机制至少需要明确以下规则:

  1. 确定复用来源和删除范围。除了字段和方法,还要说明如何处理继承来的覆盖实现、构造过程,以及 super 调用。
  2. 检查剩余实现的依赖。保留的方法、字段初始化和构造逻辑不能继续依赖被删除的成员。如果仍有依赖,应拒绝该定义,或要求提供替代实现。
  3. 重新计算类型兼容性。复用 B 的实现不能让 C 自动成为 B 的子类型。C 是否兼容某个接口,需要按独立规则判断。
  4. 定义对象布局和运行时行为。如果承诺字段不再占据对象状态,就必须构造不含该字段的对象布局,并规定反射、序列化等机制如何观察这个类。

“寻找接口匹配的最近祖先”只能解决一部分问题。一个类可能同时满足多个互不继承的接口,兼容关系未必存在唯一的“最近”节点。更合适的做法是分别判断 C 是否满足每个目标类型。

题目中的几个细节

原例中的 B 已经覆盖了 methodA()。如果 reduction 只删除 a 和 methodB(),通常应保留 B.methodA() 的实现。要恢复 A.methodA(),还需要定义“撤销覆盖”的规则。

Java 中没有访问修饰符的 int a 是包访问权限,并非 private;覆盖方法也不能随意降低原方法的访问权限。参见 JLS §8.4.8.3。

把方法改成空操作,或让它抛出异常,都没有删除该方法。它仍在 API 中,新行为是否符合原契约也需要单独判断。

如果目标包括让字段真正消失,并且不再自动属于原来的祖先类型,可以将 reduction 建模为生成新类的实现变换,再为新类单独建立类型关系。trait、模块组合,以及继承与子类型分离的研究,都可为这类设计提供参考。

备注:内容仅供参考。