伴生对象
成员函数作用于特定的类实例。有些函数不涉及“关于”一个对象的操作,因此它们不需要与该对象绑定。
在 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.i
和 WithCompanion.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
实例。a
和 b
都访问相同的内存空间来存储 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
显示了,您可以采用非 open
类 ZIClosed
,将其委托,然后重写和扩展该委托。委托将接口的方法转发给提供实现的实例。即使该实例的类是 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()
。Extended
的 ZI
部分已经通过 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.