[Kotlin] 物件表達式與宣告 Object Expressions and Declarations

有時候我們需要稍微改動一個類別,但我們不想宣告一個新的類別繼承它。Kotlin 提供物件表達式 (object expressions) 跟物件宣告 (object declarations) 來處理這種情況。

物件表達式 Object Expressions

當我們需要一個匿名類別來繼承某類別時,通常我們會這麼做。

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*...*/ }
    override fun mouseEntered(e: MouseEvent) { /*...*/ }
})

假如父類別有建構式,就必須傳入適當的參數。如果有許多父類別,它們都必須列在冒號後面並用逗號隔開。

open class A(x: Int) {
    public open val y: Int = x
}

interface B { /*...*/ }

val ab: A = object : A(1), B {
    override val y = 15
}

如果只是需要一個物件,不需要繼承任何父類別,我們可以直接這樣寫。

fun sayHi() {
    val person = object {
        var firstName: String = "Hello"
        var lastName: String = "World"
    }
    print(firstName + " " + lastName)
}

這邊有一點需要注意,匿名物件只能被使用在區域私有宣告。如果要用在一個公開函數或公開屬性,實際型態將會變成匿名物件的父類別或是 Any (如果沒指定父類別)。在這種情況下,匿名物件裡的屬性無法被存取。

class C {
    // Private function, so the return type is the anonymous object type
    private fun foo() = object {
        val x: String = "x"
    }

    // Public function, so the return type is Any
    fun publicFoo() = object {
        val x: String = "x"
    }

    fun bar() {
        val x1 = foo().x        // Works
        val x2 = publicFoo().x  // ERROR: Unresolved reference 'x'
    }
}

物件表達式裡面的程式碼可以存取外部範圍變數。

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

        override fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
    // ...
}

物件宣告 Object Declarations

單例 (singleton) 是一種很常被使用的設計模式。Kotlin 提供了很容易的方式宣告單例。

object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ...
    }

    val allDataProviders: Collection<DataProvider>
        get() = // ...
}

我們稱這為物件宣告 (object declaration)。由關鍵字 object 開頭,後面接名稱。跟一般變數宣告一樣,物件宣告不是表達式,不能被使用在等號 (=) 的右邊。

物件宣告的初始化是執行緒安全的 (thread-safe)。

直接使用名稱就可以存取這個物件。

DataProviderManager.registerDataProvider(...)

它也可以繼承父類別。

object DefaultListener : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { ... }

    override fun mouseEntered(e: MouseEvent) { ... }
}

物件宣告不能被放在區域範圍裡 (如函數),但可以被放在另一個物件宣告或非內部類別裡。

協同物件 Companion Objects

當我們宣告一個物件在類別裡時,可以替它標上 companion

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

協同物件裡的所有成員都可以直接透過類別名稱被存取。

val instance = MyClass.create()

協同物件的名稱可以省略,編譯器會直接選擇 Companion 這個字當名稱。

class MyClass {
    companion object { }
}

val x = MyClass.Companion

類別名稱本身可以當作協同物件的實體參考。

class MyClass1 {
    companion object Named { }
}

val x = MyClass1

class MyClass2 {
    companion object { }
}

val y = MyClass2

雖然協同物件的成員看起來像是其他語言的靜態成員,但在執行期間他們是真實物件的實例成員,甚至可以實作介面 (interface)。

interface Factory<T> {
    fun create(): T
}

class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}

val f: Factory<MyClass> = MyClass

在 JVM 平台上我們還是可以使用 @JvmStatic 讓協同物件的成員變成真正的靜態函數跟屬性。

物件表達式跟物件宣告的語義差別

物件表達式跟物件宣告之間有一個重要的語義差別:

  • 物件表達式是在使用它們的時候立即被執行 (及初始化)
  • 物件宣告是在第一次被用到的時候才被延遲 (lazily) 初始化。
  • 協同物件的初始化是在類別被載入時發生,跟 Java 的靜態初始化語義對應。

發表留言

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料