Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

继承与扩展

有时候继承被用来为一个类添加函数,以便为其重新赋予新的目标。这可能会导致难以理解和维护的代码。

假设有人已经创建了一个名为 Heater 的类,以及一些作用于 Heater 的函数:

// InheritanceExtensions/Heater.kt
package inheritanceextensions
import atomictest.eq

open class Heater {
  fun heat(temperature: Int) =
    "heating to $temperature"
}

fun warm(heater: Heater) {
  heater.heat(70) eq "heating to 70"
}

为了论证,想象一下 Heater 实际上比这个复杂得多,而且还有许多类似 warm() 的辅助函数。我们不想修改这个库 —— 我们想要按原样重用它。

如果我们实际上想要的是一个 HVAC(供暖、通风和空调)系统,我们可以继承 Heater 并添加一个 cool() 函数。现有的 warm() 函数以及所有作用于 Heater 的其他函数,仍然可以在我们的新类型 HVAC 上工作 —— 这在使用组合的情况下是不成立的:

// InheritanceExtensions/InheritAdd.kt
package inheritanceextensions
import atomictest.eq

class HVAC : Heater() {
  fun cool(temperature: Int) =
    "cooling to $temperature"
}

fun warmAndCool(hvac: HVAC) {
  hvac.heat(70) eq "heating to 70"
  hvac.cool(60) eq "cooling to 60"
}

fun main() {
  val heater = Heater()
  val hvac = HVAC()
  warm(heater)
  warm(hvac)
  warmAndCool(hvac)
}

这看起来很实用:Heater 没有完全满足我们的要求,所以我们从 Heater 继承了 HVAC 并附加了另一个函数。

正如您在向上转型中所见,面向对象语言具有处理继承过程中添加的成员函数的机制:在向上转型期间会去除添加的函数,使其在基类中不可用。这就是 里氏替换原则,也被称为“可替代性”,它表示接受基类的函数必须能够在不知道的情况下使用派生类的对象。可替代性是为什么 warm() 仍然在 HVAC 上起作用的原因。

尽管现代面向对象编程允许在继承过程中添加函数,但这可能是一种“代码异味”——它似乎是合理和迅速的,但可能会让您陷入麻烦。只因为它似乎有效并不意味着它是一个好主意。特别是,它可能会对以后维护代码的人(可能是您自己)产生负面影响。这种问题称为技术债务

在继承过程中添加函数在新类在整个系统中被严格视为基类时可能会很有用,而忽略了它有自己的基类。在类型检查中,您将看到在继承过程中添加函数可以是一种可行的技术。

当我们在创建 HVAC 类时,我们实际上想要的是一个带有附加 cool() 函数的 Heater 类,以便它能在 warmAndCool() 中使用。这正是扩展函数所做的事情,而无需继承:

// InheritanceExtensions/ExtensionFuncs.kt
package inheritanceextensions2
import inheritanceextensions.Heater
import atomictest.eq

fun Heater.cool(temperature: Int) =
  "cooling to $temperature"

fun warmAndCool(heater: Heater) {
  heater.heat(70) eq "heating to 70"
  heater.cool(60) eq "cooling to 60"
}

fun main() {
  val heater = Heater()
  warmAndCool(heater)
}

扩展函数不同于继承来扩展基类接口,扩展函数直接扩展基类接口,而无需继承。

如果我们对 Heater 库有控制权,我们可以以不同的方式进行设计,使其更灵活:

// InheritanceExtensions/TemperatureDelta.kt
package inheritanceextensions
import atomictest.*

class TemperatureDelta(
  val current: Double,
  val target: Double
)

fun TemperatureDelta.heat() {
  if (current < target)
    trace("heating to $target")
}

fun TemperatureDelta.cool() {
  if (current > target)
    trace("cooling to $target")
}

fun adjust(deltaT: TemperatureDelta) {
  deltaT.heat()
  deltaT.cool()
}

fun main() {
  adjust(TemperatureDelta(60.0, 70.0))
  adjust(TemperatureDelta(80.0, 60.0))
  trace eq """
    heating to 70.0
    cooling to 60.0
  """
}

在这种方法中,我们通过在多个策略中进行选择来控制温度。我们也可以将 heat()cool() 设计为成员函数,而不是扩展函数。

按约定的接口

扩展函数可以被认为是创建包含单个函数的接口:

// InheritanceExtensions/Convention.kt
package inheritanceextensions

class X

fun X.f() {}

class Y

fun Y.f() {}

fun callF(x: X) = x.f()

fun callF(y: Y) = y.f()

fun main() {
  val x = X()
  val y = Y()
  x.f()
  y.f()
  callF(x)
  callF(y)
}

现在 `X

Y都表现得好像有一个名为f()的成员函数,但我们没有获得多态行为,所以我们必须重载callF()` 以使其适用于两种类型。

这种“按约定的接口”在 Kotlin 库中被广泛使用,尤其是在处理集合时。尽管这些主要是 Java 集合,但 Kotlin 库通过添加大量的扩展函数将它们变成了函数式风格的集合。例如,在几乎任何类似集合的对象上,您都可以找到 map()reduce() 等函数。由于程序员对这个约定产生了期望,这使得编程变得更容易。

Kotlin 标准库的 Sequence 接口只包含一个成员函数。其他 Sequence 函数都是 扩展函数 —— 超过一百个。最初,这种方法用于与 Java 集合兼容,但现在它是 Kotlin 哲学的一部分:创建一个只包含定义其本质的方法的简单接口,然后将所有辅助操作都作为扩展添加进来。

适配器模式

一个库通常会定义一个类型,并提供接受该类型参数和/或返回该类型的函数:

// InheritanceExtensions/UsefulLibrary.kt
package usefullibrary

interface LibType {
  fun f1()
  fun f2()
}

fun utility1(lt: LibType) {
  lt.f1()
  lt.f2()
}

fun utility2(lt: LibType) {
  lt.f2()
  lt.f1()
}

要使用这个库,您必须以某种方式将现有的类转换为 LibType。在这里,我们从现有的 MyClass 继承,产生 MyClassAdaptedForLib,它实现了 LibType,因此可以传递给 UsefulLibrary.kt 中的函数:

// InheritanceExtensions/Adapter.kt
package inheritanceextensions
import usefullibrary.*
import atomictest.*

open class MyClass {
  fun g() = trace("g()")
  fun h() = trace("h()")
}

fun useMyClass(mc: MyClass) {
  mc.g()
  mc.h()
}

class MyClassAdaptedForLib :
  MyClass(), LibType {
  override fun f1() = h()
  override fun f2() = g()
}

fun main() {
  val mc = MyClassAdaptedForLib()
  utility1(mc)
  utility2(mc)
  useMyClass(mc)
  trace eq "h() g() g() h() g() h()"
}

尽管这确实在继承过程中扩展了一个类,但新的成员函数仅仅用于适应 UsefulLibrary。请注意,除此之外,在 MyClassAdaptedForLib 的任何地方,MyClassAdaptedForLib 的对象都可以被当作 MyClass 对象来处理,就像在调用 useMyClass() 中一样。没有代码使用扩展的 MyClassAdaptedForLib,其中基类的使用者必须知道派生类的情况。

Adapter.kt 依赖于 MyClass 被声明为 open 以供继承。如果您无法控制 MyClass,并且它不是 open,那怎么办?幸运的是,适配器也可以使用组合构建。在这里,我们在 MyClassAdaptedForLib 中添加一个 MyClass 字段:

// InheritanceExtensions/ComposeAdapter.kt
package inheritanceextensions2
import usefullibrary.*
import atomictest.*

class MyClass { // 不是 open
  fun g() = trace("g()")
  fun h() = trace("h()")
}

fun useMyClass(mc: MyClass) {
  mc.g()
  mc.h()
}

class MyClassAdaptedForLib : LibType {
  val field = MyClass()
  override fun f1() = field.h()
  override fun f2() = field.g()
}

fun main() {
  val mc = MyClassAdaptedForLib()
  utility1(mc)
  utility2(mc)
  useMyClass(mc.field)
  trace eq "h() g() g() h() g() h()"
}

这不像 Adapter.kt 那样清晰 —— 在调用 useMyClass(mc.field) 中,您必须显式地访问 MyClass 对象。但它仍然很好地解决了适应库的问题。

扩展函数似乎非常适合创建适配器。不幸的是,您不能通过收集扩展函数来实现接口。

成员函数与扩展函数

有些情况下,您被迫使用成员函数而不是扩展函数。如果一个函数必须访问一个 private 成员,您别无选择,只能将其作为成员函数:

// InheritanceExtensions/PrivateAccess.kt
package inheritanceextensions
import atomictest.eq

class Z(var i: Int = 0) {
  private var j = 0
  fun increment() {
    i++
    j++
  }
}

fun Z.decrement() {
  i--
  // j -- // 无法访问
}

成员函数 increment() 可以操作 j,但扩展函数 decrement() 无法访问 j,因为 jprivate 的。

扩展函数最大的限制是它们无法被覆盖:

// InheritanceExtensions/NoExtOverride.kt
package inheritanceextensions
import atomictest.*

open class Base {
  open fun f() = "Base.f()"
}

class Derived : Base() {
  override fun f() = "Derived.f()"
}

fun Base.g() = "Base.g()"
fun Derived.g() = "Derived.g()"

fun useBase(b: Base) {
  trace("Received ${b::class.simpleName}")
  trace(b.f())
  trace(b.g())
}

fun main() {
  useBase(Base())
  useBase(Derived())
  trace eq """
    Received Base
    Base.f()
    Base.g()
    Received Derived
    Derived.f()
    Base.g()
  """
}

trace 输出显示,多态在成员函数 f() 中起作用,但在扩展函数 g() 中则不起作用。

当一个函数不需要覆盖,并且您对类的成员有足够的访问权限时,可以将其定义为成员函数或扩展函数 —— 这是一种应该最大程度地增加代码清晰度的风格选择。

成员函数反映了类型的本质;您不能想象没有该函数的类型。扩展函数表示支持或利用该类型的“辅助”或“方便”操作,但不一定是该类型存在的必要条件。将辅助函数包含在类型内部会增加其可推理性,而将某些函数定义为扩展则使类型保持简洁和简单。

考虑一个 Device 接口。modelproductionYear 属性对于 Device 来说是固有的,因为它们描述了关键特征。诸如 overpriced()outdated() 之类的函数可以被定义为接口的成员,也可以定义为扩

展函数。在这里,它们被定义为接口的成员函数:

// InheritanceExtensions/DeviceMembers.kt
package inheritanceextensions1
import atomictest.eq

interface Device {
  val model: String
  val productionYear: Int
  fun overpriced() = model.startsWith("i")
  fun outdated() = productionYear < 2050
}

class MyDevice(
  override val model: String,
  override val productionYear: Int
): Device

fun main() {
  val gadget: Device =
    MyDevice("my first phone", 2000)
  gadget.outdated() eq true
  gadget.overpriced() eq false
}

如果我们假设 overpriced()outdated() 不会在子类中被覆盖,它们可以被定义为扩展函数:

// InheritanceExtensions/DeviceExtensions.kt
package inheritanceextensions2
import atomictest.eq

interface Device {
  val model: String
  val productionYear: Int
}

fun Device.overpriced() =
  model.startsWith("i")

fun Device.outdated() =
  productionYear < 2050

class MyDevice(
  override val model: String,
  override val productionYear: Int
): Device

fun main() {
  val gadget: Device =
    MyDevice("my first phone", 2000)
  gadget.outdated() eq true
  gadget.overpriced() eq false
}

只包含描述性成员的接口更容易理解和推理,因此第二个示例中的 Device 接口可能是一个更好的选择。然而,这最终是一个设计决策。

  • -

像 C++ 和 Java 这样的语言允许继承,除非您明确禁止。Kotlin 假设您 不会 使用继承 —— 它在不明确使用 open 关键字的情况下主动阻止继承和多态。这提供了有关 Kotlin 取向的见解:

通常,函数是您需要的一切。有时对象非常有用。对象是众多工具中的一个,但它们并不是万能的。

如果您在考虑如何在特定情况下使用继承,考虑是否真的需要继承,并应用 优先使用扩展函数和组合而不是继承(改编自书籍 设计模式)。