從Java到Kotlin(六)

陳子豪發表於2018-02-28

擴充套件與委託

目錄

1.擴充套件

  • 1.1 擴充套件函式
  • 1.2 擴充套件屬性
  • 1.3 擴充套件伴生物件
  • 1.4 擴充套件的作用域

2.委託

  • 2.1 類委託
  • 2.2 委託屬性
  • 2.3 標準委託

1.擴充套件

在Kotlin中,允許對類進行擴充套件,不需要繼承該類或使用像裝飾者這樣的任何型別的設計模式,通過一種特殊形式的宣告,來實現具體實現某一具體功能。擴充套件函式是靜態解析的,並未對原類增添函式或者屬性,對類本身沒有影響。

1.1擴充套件函式

宣告一個擴充套件函式,我們需要用一個接收者型別也就是被擴充套件的型別來作為他的字首。 下面程式碼為Kotlin原生集合類 MutableList 新增一個 swap 函式:

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // “this”對應該列表
    this[index1] = this[index2]
    this[index2] = tmp
}
複製程式碼

上面程式碼用MutableList作為接受者型別,對其進行擴充套件,為其新增一個 swap 函式,當我們呼叫 swap 函式時,可以這樣:

val mutableList = mutableListOf(1, 2, 3)
mutableList.swap(1, 2) //呼叫擴充套件函式swap()
println(mutableList)
複製程式碼

執行程式碼,得到結果

從Java到Kotlin(六)

  • 內部成員函式名與擴充套件函式名相同
    如果擴充套件函式與內部成員函式衝突,如下所示:
class User {
    fun print() {
        println("內部成員函式")
    }
}

//擴充套件函式
fun User.print() {
    println("擴充套件函式")
}

//呼叫
User().print()
複製程式碼

執行程式碼,得到結果:

從Java到Kotlin(六)
由上面例子可得,如果擴充套件函式的函式名跟內部成員函式的函式名衝突,會優先呼叫內部成員函式。

  • 可空接收者
    可以為可空的接收者型別定義擴充套件,如下所示:
//擴充套件函式
fun Any?.toString(): String {
    if (this == null) return "null"
    return toString()
}

//呼叫
var a = null
a.toString()
println(a)
var b = "not null"
b.toString()
println(b)
複製程式碼

執行程式碼,得到結果:

從Java到Kotlin(六)
上面程式碼中,我們對可空的接受者定義擴充套件,檢測呼叫者是否為null,如果為null,返回"null",如果不為null,返回Any.toString()

1.2擴充套件屬性

擴充套件屬性是對屬性的擴充套件,如下所示:

class User {
    //必須宣告為public(Kotlin預設是public)
    //否則擴充套件屬性無法訪問該變數
    var mValue = 0
}

//擴充套件屬性
var User.value: Int
    get() = mValue
    set(value) {
        mValue = value
    }

//呼叫擴充套件函式
var user = User()
user.value = 2
println(user.value)
複製程式碼

執行程式碼,得到結果:

從Java到Kotlin(六)
上面程式碼中對 mValue 進行了屬性擴充套件,抵用了擴充套件屬性實現了 setter 方法,對 mValue 進行賦值,再通過擴充套件屬性實現了 getter 方法,獲取到 mValue 的值。

1.3擴充套件伴生物件

除了擴充套件函式和擴充套件屬性外,還可以對伴生物件進行擴充套件,程式碼如下:

class User {
    companion object {
    }
}

//擴充套件伴生物件
fun User.Companion.foo() {
    println("伴生物件擴充套件")
}

//呼叫
User.foo()
複製程式碼

執行程式碼,得到結果

從Java到Kotlin(六)

1.4擴充套件的作用域

  • 在不同包裡進行擴充套件
    上面的程式碼都是在同一個包裡進行擴充套件,如果在不同包裡要進行擴充套件,就要用import來匯入資源了,如下所示:
package com.demo.czh.otherpackage

class OtherUser {

}

fun OtherUser.print() {
    println("其他包的")
}
複製程式碼

在其他包中呼叫

package com.demo.czh.activitydemo

import com.demo.czh.otherpackage.OtherUser
import com.demo.czh.otherpackage.print

User().print()
OtherUser().print()
複製程式碼

執行程式碼,得到結果:

從Java到Kotlin(六)
由上面例子可得,如果要在不用的包裡進行擴充套件,要在呼叫處 import 擴充套件的資源。

  • 擴充套件宣告為成員
    在一個類內部你可以為另一個類宣告擴充套件,如下所示:
//定義User類,新增一個printUser()函式
class User {
    fun printUser(){
        println("User")
    }
}

//定義User2類,在裡面對User類進行擴充套件
class User2 {
    fun printUser2() {
        println("User2")
    }

    //擴充套件函式
    fun User.print() {
        printUser()
        printUser2()
    }

    fun getUser(user: User) {
        //呼叫擴充套件函式
        user.print()
    }
}

//呼叫
User2().getUser(User())
複製程式碼

執行程式碼,得到結果:

從Java到Kotlin(六)
擴充套件宣告所在的類的例項稱為 分發接收者,擴充套件方法呼叫所在的接收者型別的例項稱為 擴充套件接收者 。對於分發接收者和擴充套件接收者的成員名字衝突的情況,擴充套件接收者優先。如果要引用分發接收者的成員,可以這樣寫:

//User類不變
class User {
    fun printUser(){
        println("User")
    }
}

//User2
class User2 {
    fun printUser() {
        println("User2")
    }

    fun User.print() {
        printUser()
        //表示呼叫User2的printUser()函式
        this@User2.printUser()
    }

    fun getUser(user: User) {
        //呼叫擴充套件方法
        user.print()
    }
}
複製程式碼

執行程式碼,得到結果:

從Java到Kotlin(六)
上面 User.print() 這個擴充套件函式中,用到了 限定的 this 語法來呼叫 User2 的 printUser() 函式。

  • 擴充套件成員的繼承
    宣告為成員的擴充套件可以宣告為 open 並在子類中覆蓋。這意味著這些函式的分發對於分發接收者型別是虛擬的,但對於擴充套件接收者型別是靜態的。
open class D {
}

class D1 : D() {
}

open class C {
    open fun D.foo() {
        println("D.foo in C")
    }

    open fun D1.foo() {
        println("D1.foo in C")
    }

    fun caller(d: D) {
        d.foo()   // 呼叫擴充套件函式
    }

    fun caller2(d1: D1) {
        d1.foo()   // 呼叫擴充套件函式
    }
}

class C1 : C() {
    override fun D.foo() {
        println("D.foo in C1")
    }

    override fun D1.foo() {
        println("D1.foo in C1")
    }
}

//呼叫
C().caller(D())   
C1().caller(D()) 
C().caller(D1()) 
C().caller2(D1())
C1().caller2(D1())
複製程式碼

執行程式碼,得到結果:

從Java到Kotlin(六)


2.委託

在Kotlin中,如果有多個地方用到了相同的程式碼,可以用委託來處理。

2.1類委託

委託模式是實現繼承一個很好的的替代方式,Kotlin支援委託模式,不用為了實現委託模式而編寫樣板程式碼。舉個例子:

//定義一個介面 Base
interface Base {
    fun print()
}

//定義一個 ImplBase 實現介面 Base 
class ImplBase(val i: Int) : Base {
    override fun print() {
        println(i)
    }
}

//定義一個 Drived 類實現介面 Base 
class Drived(b: Base) : Base {
    //這裡需要 override 介面 Base 裡的方法
    override fun print() {
    }
}

//如果使用委託模式的話,可以把 Base 裡的方法委託給 Drived
class Drived(b: Base) : Base by b

//呼叫 print() 方法
var b = ImplBase(10)
Drived(b).print()

//執行程式碼,列印結果為 10
複製程式碼

從上面程式碼可以看出,Derived 類通過使用 by 關鍵字將 Base 介面的 print 方法委託給物件 b ,如果不進行委託的話,則要 override Base 介面的 print 方法。 如果出現委託後仍然 override 的情況,編譯器會使用你的 override 實現取代委託物件中的實現,如下所示:

//委託後仍然 override 
class Drived(b: Base) : Base by b {
    override fun print() {
        println("abc")
    }
}

//呼叫 print() 方法
var b = ImplBase(10)
Drived(b).print()

//執行程式碼,列印結果為 abc
複製程式碼

2.2 委託屬性

在實際應用中,有很多類的屬性都擁有 getter 和 setter 函式,這些函式大部分都是相同的。Kotlin允許委託屬性,把所有相同的 getter 和 setter 函式放到同一個委託類中,這樣能大大減少冗餘程式碼。舉個例子:

class User1 {
    var userName: String = ""
        get() = field
        set(value) {
            field = value
        }
}
class User2 {
    var userName: String = ""
        get() = field
        set(value) {
            field = value
        }
}
複製程式碼

User1和User2都有相同的 getter 和 setter 函式,把它們放到委託類中,如下:

//定義一個委託類Delegate 
class Delegate {
    var userName = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("getValue  類名:$thisRef, 屬性名:${property.name}")
        return userName
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("setValue  類名:$thisRef, 屬性名:${property.name},值:$value")
        userName = value
    }
}

//將 userName 委託給 Delegate
class User1 {
    var userName: String by Delegate()
}
class User2 {
    var userName: String by Delegate()
}

//呼叫 getter 和 setter 函式
var user1 = User1()
user1.userName = "user1"
println(user1.userName)
var user2 = User2()
user2.userName = "user2"
println(user2.userName)
複製程式碼

執行程式碼,得到結果:

從Java到Kotlin(六)

可以看到,User1 和 User2 都將 userName 委託給 Delegate ,在 Delegate 內完成 getter/setter 函式,去除了相同的程式碼。

2.3 標準委託

Kotlin標準庫中提供了一些有用的委託函式:

  • 延遲委託
  • 可觀察屬性委託
  • Map委託

延遲委託

lazy()是接受一個 lambda 表示式作為引數,並返回一個 Lazy <T> 例項的函式,返回的例項作為一個委託,第一次呼叫 get() 會執行已傳遞給 lazy() 的 lambda 表示式並記錄結果, 之後再呼叫 get() 返回記錄的結果。

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

//呼叫兩次
println(lazyValue)
println(lazyValue)
複製程式碼

執行程式碼,得到結果:

從Java到Kotlin(六)
預設情況下,對於 lazy 屬性的求值是同步鎖的(synchronized):該值只在一個執行緒中計算,並且所有執行緒會看到相同的值。如果初始化委託的同步鎖不是必需的,這樣多個執行緒可以同時執行,那麼將 LazyThreadSafetyMode.PUBLICATION 作為引數傳遞給 lazy() 函式。如下所示:

val lazyValue: String by lazy(LazyThreadSafetyMode.PUBLICATION ) {
    "Hello"
}
複製程式碼

而如果你確定初始化將總是發生在單個執行緒,那麼你可以使用 LazyThreadSafetyMode.NONE 模式, 它不會有任何執行緒安全的保證和相關的開銷,如下所示:

val lazyValue: String by lazy(LazyThreadSafetyMode.NONE) {
    "Hello"
}
複製程式碼

可觀察屬性委託

實現可觀察屬性委託的函式是Delegates.observable(),當我們使用該委託函式時,可以觀察屬性的變化,如下所示:

var name: String by Delegates.observable("Czh") { property, oldValue, newValue ->
    println("屬性名:$property  舊值:$oldValue  新值:$newValue")
}

//修改name的值
name = "abc"
name = "hello"
複製程式碼

執行程式碼,得到結果:

從Java到Kotlin(六)
Delegates.observable()接收兩個引數,第一個是初始值,第二個是修改時處理程式(handler)。 每當我們給屬性賦值時會呼叫該處理程式,他有三個引數,第一個是被賦值的屬性,第二個是舊值,第三個是新值。 如果想攔截屬性的賦值操作,並且否決他的賦值操作,可以用vetoable()取代 observable(),傳遞給vetoable()的修改時處理程式會返回一個boolean型別,如果返回true,允許賦值,返回false則反之。如下所示:

var name: String by Delegates.vetoable("Czh") { property, oldValue, newValue ->
    if (newValue.equals("abc")) {
        println("屬性名:$property  舊值:$oldValue  新值:$newValue")
        true
    } else {
        println("不能修改為除了abc以外的值")
        false
    }
}

//修改name的值
name = "abc"
name = "hello"
複製程式碼

執行程式碼,得到結果:

從Java到Kotlin(六)

Map委託

Map委託是指用Map例項自身作為委託來實現委託屬性,通常用於解析 JSON ,如下所示:

//新建User類,主構函式要求傳入一個Map
class User(val map: Map<String, Any>) {
    //宣告一個 String 委託給 map
    val name: String by map
    //因為 Map 為只讀,所以只能用 val 宣告
    val age: Int     by map
}

var map = mapOf("name" to "Czh", "age" to 22)
var user = User(map)
println("${user.name}  ${user.age}")  
//列印結果為  Czh  22
複製程式碼

因為Map只有getValue方法而沒有setValue方法,所以不能通過User物件設定值,這時可以把User的主構函式改為傳入一個MutableMap,並把屬性委託給MutableMap,如下所示:

class User(val map: MutableMap<String, Any>) {
    //因為MutableMap為讀寫,可以用var宣告
    var name: String by map
    var age: Int     by map
}

var map = mutableMapOf("name" to "Czh", "age" to 22)
var user = User(map)
user.name = "James Harden"
user.age = 28
println("${user.name}  ${user.age}")
//列印結果為  James Harden  28
複製程式碼

總結

本篇文章簡述了Kotlin中擴充套件和委託的使用方法。擴充套件和委託都是Kotlin自身支援並非常好用的,擴充套件能使程式碼更靈活,委託能實現程式碼重用。運用好他們能很好地加快編寫程式碼的速度。

參考文獻:
Kotlin語言中文站、《Kotlin程式開發入門精要》

推薦閱讀:
從Java到Kotlin(一)為什麼使用Kotlin
從Java到Kotlin(二)基本語法
從Java到Kotlin(三)類和介面
從Java到Kotlin(四)物件與泛型
從Java到Kotlin(五)函式與Lambda表示式
從Java到Kotlin(六)擴充套件與委託
從Java到Kotlin(七)反射和註解
從Java到Kotlin(八)Kotlin的其他技術
Kotlin學習資料總彙


更多精彩文章請掃描下方二維碼關注微信公眾號"AndroidCzh":這裡將長期為您分享原創文章、Android開發經驗等! QQ交流群: 705929135

從Java到Kotlin(六)

相關文章