[譯] Kotlin中關於Companion Object的那些事

極客熊貓發表於2019-04-10

翻譯說明:

原標題: A few facts about Companion objects

原文地址: blog.kotlin-academy.com/a-few-facts…](blog.kotlin-academy.com/a-few-facts…)

原文作者: David Blanc

Kotlin給Java開發者帶來最大改變之一就是廢棄了static修飾符。與Java不同的是在Kotlin的類中不允許你宣告靜態成員或方法。相反,你必須向類中新增Companion物件來包裝這些靜態引用: 差異看起來似乎很小,但是它有一些明顯的不同。

[譯] Kotlin中關於Companion Object的那些事

首先,companion伴生物件是個實際物件的單例例項。你實際上可以在你的類中宣告一個單例,並且可以像companion伴生物件那樣去使用它。這就意味著在實際開發中,你不僅僅只能使用一個靜態物件來管理你所有的靜態屬性! companion這個關鍵字實際上只是一個快捷方式,允許你通過類名訪問該物件的內容(如果伴生物件存在一個特定的類中,並且只是用到其中的方法或屬性名稱,那麼伴生物件的類名可以省略不寫)。就編譯而言,下面的testCompanion()方法中的三行都是有效的語句。

class TopLevelClass {

    companion object {
        fun doSomeStuff() {
            ...
        }
    }

    object FakeCompanion {
        fun doOtherStuff() {
            ...
        }
    }
}

fun testCompanion() {
    TopLevelClass.doSomeStuff()
    TopLevelClass.Companion.doSomeStuff()
    TopLevelClass.FakeCompanion.doOtherStuff()
}
複製程式碼

為了相容的公平性,companion關鍵字還提供了更多選項,尤其是與Java互操作性相關選項。果您嘗試在Java類中編寫相同的測試程式碼,呼叫方式可能會略有不同:

public void testCompanion() {
    TopLevelClass.Companion.doSomeStuff();
    TopLevelClass.FakeCompanion.INSTANCE.doOtherStuff();
}
複製程式碼

區別在於: Companion作為Java程式碼中靜態成員開放(實際上它是一個物件例項,但是由於它的名稱是以大寫的C開頭,所以有點存在誤導性),而FakeCompanion引用了我們的第二個單例物件的類名。在第二個方法呼叫中,我們需要使用它的INSTANCE屬性來實際訪問Java中的例項(你可以開啟IntelliJ IDEA或AndroidStudio中的"Show Kotlin Bytecode"選單欄,並點選裡面"Decompile"按鈕來檢視反編譯後對應的Java程式碼)

在這兩種情況下(不管是Kotlin還是Java),使用伴生物件Companion類比FakeCompanion類那種呼叫語法更加簡短。此外,由於Kotlin提供一些註解,可以讓編譯器生成一些簡短的呼叫方式,以便於在Java程式碼中依然可以像在Kotlin中那樣簡短形式呼叫。

@JvmField註解,例如告訴編譯器不要生成gettersetter,而是生成Java中成員。在伴生物件的作用域內使用該註解標記某個成員,它產生的副作用是標記這個成員不在伴生物件內部作用域,而是作為一個Java最外層類的靜態成員存在。從Kotlin的角度來看,這沒有什麼太大區別,但是如果你看一下反編譯的位元組程式碼,你就會注意到伴生物件以及他的成員都宣告和最外層類的靜態成員處於同一級別。

另一個有用的註解 @JvmStatic.這個註解允許你呼叫伴生物件中宣告的方法就像是呼叫外層的類的靜態方法一樣。但是需要注意的是:在這種情況下,方法不會和上面的成員一樣移出伴生物件的內部作用域。因為編譯器只是向外層類中新增一個額外的靜態方法,然後在該方法內部又委託給伴生物件。

一起來看一下這個簡單的Kotlin類例子:

class MyClass {
    companion object {
        @JvmStatic
        fun aStaticFunction() {}
    }
}
複製程式碼

這是相應編譯後的Java簡化版程式碼:

public class MyClass {
    public static final MyClass.Companion Companion = new MyClass.Companion();
    fun aStaticFunction() {//外層類中新增一個額外的靜態方法
        Companion.aStaticFunction();//方法內部又委託給伴生物件的aStaticFunction方法
    }
    public static final class Companion {
         public final void aStaticFunction() {}
    }
}
複製程式碼

這裡存在一個非常細微的差別,但在某些特殊的情況下可能會出問題。例如,考慮一下Dagger中的module(模組)。當定義一個Dagger模組時,你可以使用靜態方法去提升效能,但是如果你選擇這樣做,如果您的模組包含靜態方法以外的任何內容,則編譯將失敗。由於Kotlin在類中既包含靜態方法,也保留了靜態伴生物件,因此無法以這種方式編寫僅僅包含靜態方法的Kotlin類。

但是不要那麼快放棄! 這並不意味著你不能這樣做,只是它需要一個稍微不同的處理方式:在這種特殊的情況下,你可以使用Kotlin單例(使用object物件表示式而不是class類)替換含有靜態方法的Java類並在每個方法上使用@JvmStatic註解。如下例所示:在這種情況下,生成的位元組程式碼不再顯示任何伴生物件,靜態方法會附加到類中。

@Module
object MyModule {

    @Provides
    @Singleton
    @JvmStatic
    fun provideSomething(anObject: MyObject): MyInterface {
        return myObject
    }
}
複製程式碼

這又讓你再一次明白了伴生物件僅僅是單例物件的一個特例。但它至少表明與許多人的認知是相反的,你不一定需要一個伴生物件來維護靜態方法或靜態變數。你甚至根本不需要一個物件來維護,只要考慮頂層函式或常量:它們將作為靜態成員被包含在一個自動生成的類中(預設情況下,例如MyFileKt會作為MyFile.kt檔案生成的類名,一般生成類名以Kt為字尾結尾)

我們有點偏離這篇文章的主題了,所以讓我們繼續回到伴生物件上來。現在你已經瞭解了伴生物件實質就是物件,也應該意識到它開放了更多的可能性,例如繼承和多型。

這意味著你的伴生物件並不是沒有型別或父類的匿名物件。它不僅可以擁有父類,而且它甚至可以實現介面以及含有物件名。它不需要被稱為companion。這就是為什麼你可以這樣寫一個Parcelable類:

class ParcelableClass() : Parcelable {

    constructor(parcel: Parcel) : this()

    override fun writeToParcel(parcel: Parcel, flags: Int) {}

    override fun describeContents() = 0

    companion object CREATOR : Parcelable.Creator<ParcelableClass> {
        override fun createFromParcel(parcel: Parcel): ParcelableClass = ParcelableClass(parcel)

        override fun newArray(size: Int): Array<ParcelableClass?> = arrayOfNulls(size)
    }
}
複製程式碼

這裡, 伴生物件名為CREATOR,它實現了Android中的Parcelable.Creator介面,允許遵守Parcelable約定,同時保持比使用@JvmField註釋在伴隨物件內新增Creator物件更直觀。Kotlin中引入了@Parcelize註解,以便於可以獲得所有樣板程式碼,但是在這不是重點...

為了使它變得更簡潔,如果你的伴生物件可以實現介面,它甚至可以使用Kotlin中的代理來執行此操作:

class MyObject {
    companion object : Runnable by MyRunnable()
}
複製程式碼

這將允許您同時向多個物件中新增靜態方法!請注意,伴生物件在這種情況下甚至不需要作用域體,因為它是由代理提供的。

最後但同樣重要的是,你可以為伴生物件定義擴充套件函式! 這就意味著你可以在現有的類中新增靜態方法或靜態屬性,如下例所示:

class MyObject {

    companion object

    fun useCompanionExtension() {
        someExtension()
    }

}

fun MyObject.Companion.someExtension() {}//定義擴充套件函式
複製程式碼

這樣做有什麼意義?我真的不知道。雖然Marcin Moskala建議使用此操作將靜態工廠方法以Companion的擴充套件函式的形式新增到類中。

總而言之,伴生物件不僅僅是為了給缺少static修飾符的使用場景提供解決方案:

  • 它們是真正的Kotlin物件,包括名稱和型別,以及一些額外的功能。
  • 他們甚至可以不用於僅僅為了提供靜態成員或方法場景。可以有更多其他選擇,比如他們可以用作單例物件或替代頂層函式的功能。

與大多數場景一樣,Kotlin意味著在你設計過程需要有一點點轉變,但與Java相比,它並沒有真正限制你的選擇。如果有的話,也會通過提供一些新的、更簡潔的方式讓你去使用它。

[譯] Kotlin中關於Companion Object的那些事

歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~

Kotlin系列文章,歡迎檢視:

Kotlin邂逅設計模式系列:

資料結構與演算法系列:

Kotlin 原創系列:

Effective Kotlin翻譯系列

翻譯系列:

實戰系列:

相關文章