Kotlin對象表達式與對象聲明、委托、委托屬性

一、對象表達式

要創建?個繼承自某個(或某些)類型的匿名類的對象:

//對象表達式中的代碼可以訪問來自包含它的作用域的變量
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++
        }
    })// ……
}

如果超類型有?個構造函數,則必須傳遞適當的構造函數參數給它。多個超類型可以由跟在冒號后面的逗號分隔的列表指定:

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 foo() {
    val adHoc = object {
        var x: Int = 0 
        var y: Int = 0
    } 
    print (adHoc.x + adHoc.y)
}

匿名對象可以用作只在本地和私有作用域中聲明的類型。如果你使用匿名對象作為公有函數的返回類型或者用作公有屬性的類型,那么該函數或屬性的實際類型會是匿名對象聲明的超類型,如果你沒有聲明任何超類型,就會是 Any ,所以在匿名對象中添加的成員將無法訪問。

class C {
    // 私有函數,所以其返回類型是匿名對象類型 
    private fun foo() = object {
        val x: String = "x"
    }

    // 公有函數,所以其返回類型是Any 
    fun publicFoo() = object {
        val x: String = "x"
    }

    fun bar() {
        val x1 = foo().x // 沒問題 
        val x2 = publicFoo().x // 錯誤:未能解析的引用“x” 
    }
}

二、對象聲明

總是在 object 關鍵字后跟?個名稱。對象聲明的初始化過程是線程安全的并且在首次訪問時進行。
對象聲明不能在局部作用域(即直接嵌套在函數內部),但是它們可以嵌套到其他對象聲明或非內部類中。

1.伴生對象

  • companion 關鍵字標記
  • 伴生對象的成員可通過只使用類名作為限定符來調用
  • 省略伴生對象的名稱,在這種情況下將使用名稱 Companion
  • 其類的名稱可用作對該類的伴生對象的引用
  • 伴生對象的成員是真實對象的實例成員,還可實現接口
  • 使用@JvmStatic 注解,你可以將伴生對象的成員生成為真正的靜態方法和字段
interface Factory<T> {
    fun create(): T
}

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

val f: Factory<MyClass> = MyClass
val companionClassA = MyClass
val companionClassB = MyClass.Companion

2.對象表達式和對象聲明之間的語義差異

  • 對象表達式是在使用他們的地方立即執行(及初始化)的
  • 對象聲明是在第?次被訪問到時延遲初始化的
  • 伴生對象的初始化是在相應的類被加載(解析)時,與 Java 靜態初始化器的語義相匹配

三、類型別名

類型別名為現有類型提供替代名稱。如果類型名稱太長,你可以另外引入較短的名稱,并使用新的名稱替代原類型名。類型別名不會引入新類型。它們等效于相應的底層類型

//縮減集合類型:
typealias NodeSet = Set<Network.Node> 
typealias FileTable<K> = MutableMap<K, MutableList<File>>

//函數類型提供另外的別名:
typealias MyHandler = (Int, String, Any) -> Unit 
typealias Predicate<T> = (T) -> Boolean

//嵌套類和內部類創建新名稱:
class A {
    class Inner
}

class B {
    inner class Inner
}
typealias AInner = A.Inner
typealias BInner = B.Inner

fun foo(p: Predicate<Int>) = p(42)
fun main() {
    val f: (Int) -> Boolean = { it > 0 }
    println(foo(f)) // 輸出 "true"

    val p: Predicate<Int> = { it > 0 }
    println(listOf(1, -2).filter(p)) // 輸出 "[1]"
}

四、委托

1.由委托實現

委托模式已經證明是實現繼承的?個很好的替代方式,而 Kotlin 可以零樣板代碼地原生支持它。Derived 類 可以通過將其所有公有成員都委托給指定對象來實現?個接口Base:

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() {
        print(x)
    }
}

class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10) 
    Derived (b).print()
}

Derived 的超類型列表中的 by-子句表式 b 將會在 Derived 中內部存儲,并且編譯器將生成轉發給 b 的所有 Base 的方法。

2.覆蓋由委托實現的接口成員

覆蓋符合預期:編譯器會使用 override 覆蓋的實現而不是委托對象中的。重寫的成員不會在委托對象的成員中調用 ,委托對象的成員只能訪問其自身對接口成員

interface BaseA {
    val message: String
    fun print()
}

class BaseImpl(x: Int) : BaseA {
    override val message = "BaseImpl: x = $x"
    override fun print() {
        println(message)
    }
}

class DerivedA(b: BaseA) : BaseA by b {
    // 在 b 的 `print` 實現中不會訪問到這個屬性 
    override val message = "Message of Derived"
}

fun main() {
    val b = BaseImpl(10)
    val derived = DerivedA(b)
    derived.print()
    println(derived.message)
}

3.委托屬性

有?些常見的屬性類型,雖然我們可以在每次需要的時候手動實現它們,但是如果能夠為大家把他們只實現?次并放入?個庫會更好。例如:

  • 延遲屬性(lazy properties):其值只在首次訪問時計算;
  • 可觀察屬性(observable properties): 監聽器會收到有關此屬性變更的通知;
  • 把多個屬性儲存在?個映射(map)中,而不是每個存在單獨的字段中。

語法是:val/var <屬性名>: <類型> by <表達式> 。在 by 后面的表達式是該委托,因為屬性對應的 get()(與 set() )會被委托給它的 getValue() 與 setValue() 方法。屬性的委托不必實現任何的接口,但是需要提供?個 getValue() 函數(與 setValue() ——對于 var 屬性)

class Example {
    var p: String by Delegate()
}

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

fun main() {
    val e = Example()
    println(e.p)
    e.p = "NEW"
}

輸出:
Example@33a17727, thank you for delegating ‘p’ to me!
NEW has been assigned to ‘p’ in Example@33a17727.

4.標準委托:延遲屬性 Lazy

lazy() 是接受?個 lambda 并返回?個 Lazy <T> 實例的函數,返回的實例可以作為實現延遲屬性的委托: 第?次調? get() 會執?已傳遞給 lazy() 的 lambda 表達式并記錄結果,后續調? get() 只是返回記 錄的結果。

val lazyValue: String by lazy {
    println("MainActivity computed!")
    return@lazy "Hello"    //return@lazy 可以省略
}

fun main() {
    println(lazyValue)
    println(lazyValue)
}

lazy 屬性的求值是同步鎖的(synchronized):該值只在?個線程中計算,并且所有線程會看到相同的值。
LazyThreadSafetyMode.PUBLICATION設置多個線程可以同步初始化委托。如果確定初始化將總是發生在與屬性使用位于相同的線程,可以使用LazyThreadSafetyMode.NONE 模式:它不會有任何線程安全的保證以及相關的開銷。

5.標準委托:可觀察屬性 Observable

Delegates.observable() 接受兩個參數:初始值與修改時處理程序(handler)。每當我們給屬性賦值時會 調?該處理程序(在賦值后執?)。它有三個參數:被賦值的屬性、舊值與新值:

class User {
    var name: String by Delegates.observable("<no name>") { prop, old, new ->
        println("$old -> $new")
    }
}

fun main() {
    val user = User()
    user.name = "first"
    user.name = "second"
}

截獲賦值并“否決”它們,那么使用 vetoable() 取代 observable() 。在屬性被賦新值生效之前會調用傳遞給 vetoable 的處理程序。

6.委托給另?個屬性

?個屬性可以把它的 getter 與 setter 委托給另?個屬性。這種委托對于頂層和類的屬性(成員和擴展)都可用。該委托屬性可以為:

  • 頂層屬性
  • 同?個類的成員或擴展屬性
  • 另?個類的成員或擴展屬性
    為將?個屬性委托給另?個屬性,應在委托名稱中使用恰當的 :: 限定符
class ClassWithDelegate {
    val anotherClassInt = 0
}

var topLevelInt: Int = 0

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
    var delegatedToMember: Int by this::memberInt
    var delegatedToTopLevel: Int by ::topLevelInt
    val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}

var MyClass.extDelegated: Int by ::topLevelInt

當想要以?種向后兼容的方式重命名?個屬性的時候:引入?個新的屬性、使用 @Deprecated 注解來注解舊的屬性、并委托其實現。

class MyCla {
    var newName: Int = 0

    @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
    var oldName: Int by this::newName
}

fun main() {
    val myClass = MyCla() // 通知:'oldName: Int' is deprecated.
    // Use 'newName' instead
    myClass.oldName = 42
    println(myClass.newName) //42
}

7.將屬性儲存在映射中

?個常見的用例是在?個映射(map)里存儲屬性的值。這經常出現在像解析 JSON 或者做其他“動態”事情的應用中。在這種情況下,你可以使用映射實例自身作為委托來實現委托屬性。

class User(map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
}

val user = User(mapOf("name" to "John Doe", "age" to 25))

fun main() {
    println(user.name)  // Prints "John Doe"
    println(user.age)   // Prints 25
}

//var屬性,需要把只讀的 Map 換成 MutableMap
class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int by map
}

8.局部委托屬性

可以將局部變量聲明為委托屬性,如by lazy?個局部變量惰性初始化。

9.屬性委托要求

對于?個只讀屬性(即 val 聲明的),委托必須提供?個操作符函數 getValue() ,該函數具有以下參數:

  • thisRef —— 必須與屬性所有者類型(對于擴展屬性——指被擴展的類型)相同或者是其超類型。
  • property —— 必須是類型 KProperty<*> 或其超類型。
    getValue() 必須返回與屬性相同的類型(或其子類型)。

對于?個可變屬性(即 var 聲明的),委托必須額外提供?個操作符函數 setValue() ,該函數具有以下參數:

  • thisRef —— 必須與屬性所有者類型(對于擴展屬性——指被擴展的類型)相同或者是其超類型。
  • property —— 必須是類型 KProperty<*> 或其超類型。
  • value — 必須與屬性類型相同(或者是其超類型)。

getValue() 或/與 setValue() 函數可以通過委托類的成員函數提供或者由擴展函數提供。當你需要委托屬性到原本未提供的這些函數的對象時后者會更便利。兩函數都需要用 operator 關鍵字來進行標記。

翻譯規則

在每個委托屬性的實現的背后,Kotlin 編譯器都會生成輔助屬性并委托給它。例如,對于屬性 prop,生成隱藏 屬性 prop$delegate ,而訪問器的代碼只是簡單地委托給這個附加屬性:

class C {
    var prop: Type by MyDelegate()
}

// 這段是由編譯器?成的相應代碼:
class C {
    private val prop$delegate = MyDelegate()
    var prop: Type get() = prop$delegate.getValue(this, this::prop)
    set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

編譯器在參數中提供了關于 prop 的所有必要信息:第?個參數 this 引用到外部類 C 的實例而 this::prop 是 KProperty 類型的反射對象,該對象描述 prop 自身

提供委托

通過定義 provideDelegate 操作符,可以擴展創建屬性實現所委托對象的邏輯。如果 by 右側所使?的對 象將 provideDelegate 定義為成員或擴展函數,那么會調?該函數來創建屬性委托實例。

class ResourceDelegate<T> : ReadOnlyProperty<MyUI, T> {
    override fun getValue(thisRef: MyUI, property: KProperty<*>): T {
        ...
    }
}

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(thisRef: MyUI, prop: KProperty<*>): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        // 創建委托 
        return ResourceDelegate()
    }

    private fun checkProperty(thisRef: MyUI, name: String) {
        ……
    }
}

class MyUI {
    fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> {
        ……
    }

    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

provideDelegate 的參數與 getValue 相同:

  • thisRef —— 必須與 屬性所有者 類型(對于擴展屬性——指被擴展的類型)相同或者是它的超類型;
  • property —— 必須是類型 KProperty<*> 或其超類型。
    在創建 MyUI 實例期間,為每個屬性調用provideDelegate 方法,并立即執行必要的驗證
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370