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

伴生对象

成员函数作用于特定的类实例。有些函数不涉及“关于”一个对象的操作,因此它们不需要与该对象绑定。

companion object 内部定义的函数和字段与该类有关。常规类元素可以访问伴生对象的元素,但伴生对象的元素不能访问常规类的元素。

正如您在对象中看到的那样,可以在类内部定义一个常规的 object,但这不会在 object 和类之间提供关联。特别是,在引用其成员时,您必须显式地为嵌套的 object 命名。如果在类内部定义一个伴生对象,它的元素将透明地对该类可用:

// CompanionObjects/CompanionObject.kt
package companionobjects
import atomictest.eq

class WithCompanion {
  companion object {
    val i = 3
    fun f() = i * 3
  }
  fun g() = i + f()
}

fun WithCompanion.Companion.h() = f() * i

fun main() {
  val wc = WithCompanion()
  wc.g() eq 12
  WithCompanion.i eq 3
  WithCompanion.f() eq 9
  WithCompanion.h() eq 27
}

在类外部,您可以使用类名访问伴生对象的成员,就像 WithCompanion.iWithCompanion.f() 中所示。类的其他成员可以在不限定的情况下访问伴生对象的元素,就像 g() 的定义中所示。

h() 是伴生对象的扩展函数。

如果一个函数不需要访问私有类成员,您可以选择将其定义在文件范围内,而不是将其放在伴生对象中。

每个类只允许有一个伴生对象。为了清晰起见,您可以给伴生对象起一个名字:

// CompanionObjects/NamingCompanionObjects.kt
package companionobjects
import atomictest.eq

class WithNamed {
  companion object Named {
    fun s() = "from Named"
  }
}

class WithDefault {
  companion object {
    fun s() = "from Default"
  }
}

fun main() {
  WithNamed.s() eq "from Named"
  WithNamed.Named.s() eq "from Named"
  WithDefault.s() eq "from Default"
  // 默认名称是 "Companion":
  WithDefault.Companion.s() eq "from Default"
}

即使在给伴生对象起了名字,您仍然可以在不使用该名称的情况下访问其元素。如果不给伴生对象起名称,Kotlin 将为其分配名称 Companion

如果在伴生对象中创建属性,它将产生一个单一的存储空间,用于该字段,并与关联类的所有实例共享:

// CompanionObjects/ObjectProperty.kt
package companionobjects
import atomictest.eq

class WithObjectProperty {
  companion object {
    private var n: Int = 0 // 只有一个
  }
  fun increment() = ++n
}

fun main() {
  val a = WithObjectProperty()
  val b = WithObjectProperty()
  a.increment() eq 1
  b.increment() eq 2
  a.increment() eq 3
}

main() 中的测试显示,n 只有一个存储空间,不管创建了多少个 WithObjectProperty 实例。ab 都访问相同的内存空间来存储 n

increment() 显示可以从其封闭类中访问伴生对象的 private 成员。

当一个函数访问伴生对象中的属性时,将该函数移到伴生对象中是有意义的:

// CompanionObjects/ObjectFunctions.kt
package companionobjects
import atomictest.eq

class CompanionObjectFunction {
  companion object {
    private var n: Int = 0
    fun increment() = ++n
  }
}

fun main() {
  CompanionObjectFunction.increment() eq 1
  CompanionObjectFunction.increment() eq 2
}

您不再需要一个 CompanionObjectFunction 实例来调用 increment()

假设您想要保持创建的每个对象的计数,以给每个对象赋予唯一的可读标识符:

// CompanionObjects/ObjectCounter.kt
package companionobjects
import atomictest.eq

class Counted {
  companion object {
    private var count = 0
  }
  private val id = count++
  override fun toString() = "#$id"
}

fun main() {
  List(4) { Counted() } eq "[#0, #1, #2, #3]"
}

伴生对象可以是在其他地方定义的类的实例:

// CompanionObjects/CompanionInstance.kt
package companionobjects
import atomictest.*

interface ZI {
  fun f(): String
  fun g(): String
}

open class ZIOpen : ZI {
  override fun f() = "ZIOpen.f()"
  override fun g() = "ZIOpen.g()"
}

class ZICompanion {
  companion object: ZIOpen()
  fun u() = trace("${f()} ${g()}")
}

class ZICompanionInheritance {
  companion object: ZIOpen() {
    override fun g() =
      "ZICompanionInheritance.g()"
    fun h() = "ZICompanionInheritance.h()"
  }
  fun u() = trace("${f()} ${g()} ${h()}")
}

class ZIClass {
  companion object: ZI {
    override fun f() = "ZIClass.f()"
    override fun g() = "ZIClass.g()"
  }
  fun u() = trace("${f()} ${g()}")
}

fun main() {
  ZIClass.f()
  ZIClass.g()
  ZIClass().u()
  ZICompanion.f()
  ZICompanion.g()
  ZICompanion().u()
  ZICompanionInheritance.f()
  ZICompanionInheritance.g()
  ZICompanionInheritance().u()
  trace eq """
    ZIClass.f() ZIClass.g()
    ZIOpen.f() ZIOpen.g()
    ZIOpen.f()
   

 ZICompanionInheritance.g()
    ZICompanionInheritance.h()
  """
}

ZICompanion 使用一个 ZIOpen 对象作为其伴生对象,而 ZICompanionInheritance 在重写和扩展 ZIOpen 的同时创建了一个 ZIOpen 对象。ZIClass 显示了您可以在创建伴生对象的同时实现接口。

如果您想要用作伴生对象的类不是 open,则不能像上面那样直接使用它。然而,如果该类实现了一个接口,您仍然可以通过类委托使用它:

// CompanionObjects/CompanionDelegation.kt
package companionobjects
import atomictest.*

class ZIClosed : ZI {
  override fun f() = "ZIClosed.f()"
  override fun g() = "ZIClosed.g()"
}

class ZIDelegation {
  companion object: ZI by ZIClosed()
  fun u() = trace("${f()} ${g()}")
}

class ZIDelegationInheritance {
  companion object: ZI by ZIClosed() {
    override fun g() =
      "ZIDelegationInheritance.g()"
    fun h() =
      "ZIDelegationInheritance.h()"
  }
  fun u() = trace("${f()} ${g()} ${h()}")
}

fun main() {
  ZIDelegation.f()
  ZIDelegation.g()
  ZIDelegation().u()
  ZIDelegationInheritance.f()
  ZIDelegationInheritance.g()
  ZIDelegationInheritance().u()
  trace eq """
    ZIClosed.f() ZIClosed.g()
    ZIClosed.f()
    ZIDelegationInheritance.g()
    ZIDelegationInheritance.h()
  """
}

ZIDelegationInheritance 显示了,您可以采用非 openZIClosed,将其委托,然后重写和扩展该委托。委托将接口的方法转发给提供实现的实例。即使该实例的类是 final,我们仍然可以对委托接收者进行重写并添加方法。

下面是一个小谜题:

// CompanionObjects/DelegateAndExtend.kt
package companionobjects
import atomictest.eq

interface Extended: ZI {
  fun u(): String
}

class Extend : ZI by Companion, Extended {
  companion object: ZI {
    override fun f() = "Extend.f()"
    override fun g() = "Extend.g()"
  }
  override fun u() = "${f()} ${g()}"
}

private fun test(e: Extended): String {
  e.f()
  e.g()
  return e.u()
}

fun main() {
  test(Extend()) eq "Extend.f() Extend.g()"
}

Extend 中,ZI 接口是使用它自己的 companion object 来实现的,该对象的默认名称是 Companion。但我们还要实现 Extended 接口,该接口是 ZI 接口加上一个额外的函数 u()ExtendedZI 部分已经通过 Companion 实现,因此我们只需要 override 附加函数 u() 来完成 Extend。现在,Extend 对象可以向上转型为 Extended,作为 test() 的参数。

伴生对象的一个常见用法是控制对象的创建——这就是工厂方法模式。假设您只想允许创建 Numbered2 对象的列表,而不允许单独创建 Numbered2 对象:

// CompanionObjects/CompanionFactory.kt
package companionobjects
import atomictest.eq

class Numbered2
private constructor(private val id: Int) {
  override fun toString(): String = "#$id"
  companion object Factory {
    fun create(size: Int) =
      List(size) { Numbered2(it) }
  }
}

fun main() {
  Numbered2.create(0) eq "[]"
  Numbered2.create(5) eq
    "[#0, #1, #2, #3, #4]"
}

Numbered2 构造函数是 private 的。这意味着只有一种方式可以创建实例——通过 create() 工厂函数。工厂函数有时可以解决常规构造函数无法解决的问题。

伴生对象中的构造函数在程序中首次实例化封闭类时初始化:

// CompanionObjects/Initialization.kt
package companionobjects
import atomictest.*

class CompanionInit {
  companion object {
    init {
      trace("Companion Constructor")
    }
  }
}

fun main() {
  trace("Before")
  CompanionInit()
  trace("After 1")
  CompanionInit()
  trace("After 2")
  CompanionInit()
  trace("After 3")
  trace eq """
    Before
    Companion Constructor
    After 1
    After 2
    After 3
  """
}

从输出可以看出,在首次创建 CompanionInit() 对象时,伴生对象仅被构造一次。

Exercises and solutions can be found at www.AtomicKotlin.com.