1. 引入:委託作爲一種傳統的設計模式,在Java中要想實現這種設計模式,就需要自己進行類的結構設計來實現。而在Kotlin中,提供語言層面上的支持,我們可以通過by關鍵字很輕鬆就能實現。
2. 類委託(class delegate)
-
自己動手實現委託:
interface ServerApi { fun login(username: String, password: String) } class Retrofit : ServerApi { /*類比登錄操作 */ override fun login(username: String, password: String) { println("login successfully.") } } class RemoteRepository : ServerApi { private val serverApi: ServerApi = Retrofit() override fun login(username: String, password: String) { serverApi.login(username, password) } } fun main() { val repository = RemoteRepository() repository.login("David", "123456") //輸出 login successfully. }
首先我們聲明瞭一個接口ServerApi,然後定義了其兩個實現類:Retrofit、RemoteRepository,並且我們在Retrofit類裏實現了具體的login方法,而在RemoteRepository裏對於login方法的實現邏輯就直接委託給了serverApi(Retrofit對象),這樣我們就自己實現了一個委託模式。但是我們發現:無論我們在ServerApi中定義多少個抽象方法,RemoteRepository類的結構是有規律可言的,或者不禮貌地說這些代碼比較冗餘。那下面我們就來看看如何通過by關鍵字輕鬆實現類委託。
-
使用by關鍵字,改寫RemoteRepository類:
class RemoteRepository(retrofit: Retrofit) : ServerApi by retrofit
搞定。可以看出語法就是:在要實現的接口後面 + by + 委託對象。這樣我們使用了Kotlin的類委託。
-
透過現象看本質:
按照慣例,我們現在去反編譯,從字節碼層面上去理解Kotlin的類委託。實際上大家可以去猜測一下底層實現(應該和前面我們手動實現的一樣):
的確,和我們之前自己手動實現的一樣:編譯器會自動在被委託類添加了一個委託類對象作爲它的屬性,並且在構造方法中將我們指定的委託對象賦值給了它,然後實現了抽象方法,實現的邏輯就是委託給這個添加的委託類對象。
-
對於被委託類中某些方法,可以提供自己的實現
interface ServerApi { fun login(username: String, password: String) fun register(username: String, password: String) } class Retrofit : ServerApi { override fun login(username: String, password: String) { println("login: username = $username, password = $password") } override fun register(username: String, password: String) { println("register: username = $username, password = $password") } } class RemoteRepository(retrofit: Retrofit) : ServerApi by retrofit{ override fun register(username: String, password: String) { println("register in RemoteRepository.") } }
這樣的話,對於register的調用,就會執行自己的邏輯,編譯器就不會再爲你提供實現。
-
多個委託:
interface NewsApi { fun getNewsList() } class NewsApiImpl : NewsApi { override fun getNewsList() { println("NewsApiImpl: getNewsList()") } } class RemoteRepository(retrofit: Retrofit) : ServerApi by retrofit, NewsApi by NewsApiImpl()
如果需要多個委託,採用這種語法就可以,一一對應。
3. 屬性委託(delegated property)
- 引入:在kotlin中,不光光支持類委託,對於屬性的訪問(set、get),我們也可以指定它的委託
-
如何實現屬性委託:
class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>) = "hello world" operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("the new value is $value") } } class RealSub { var str: String by Delegate() } fun main() { val sub = RealSub() sub.str = "hello" println(sub.str) }
實際上可以分爲兩步:
a. 定義一個屬性委託類(如這裏的Delegate),然後在這個類中提供兩個方法:getValue()、setValue(),他們的方法簽名必須按照如下格式:
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {} operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {}
operator關鍵字:表示運算符重載,可參考此篇文章
第一個參數:表示被委託屬性所屬的對象
第二個參數:類似Java中的Field,屬性的反射對象
第三個參數(setValue中):被委託屬性對象要賦的值
返回值(getValue中):被委託屬性對象的值b. 在被委託的屬性(如這裏的str)後添加:by 委託對象
我們再來看main方法的測試代碼:
sub.str = "hello",就會觸發setValue方法的調用,打印:the new value is hello
println(sub.str),就會觸發getValue方法的調用,返回"hello world", 故打印:hello world總結來說,屬性委託就是對於某屬性的訪問,委託給了我們指定的委託對象中的getValue、setValue(var類型屬性)方法。剛剛我們是自己實現了這個了屬性委託,實際上Kotlin標準庫中也爲了提供幾種常用的屬性委託,能夠爲我們的開發帶來極大的便利。
4. 標準庫中的屬性委託之:lazy
- 使用場景:延遲val屬性的初始化時機(第一次訪問的時候纔會去初始化)
-
示例代碼:
class LazyTest { val lazyValue: String by lazy { println("I'm in lazy.") "lazyValue" } } fun main() { val lazyTest = LazyTest() println(lazyTest.lazyValue) println(lazyTest.lazyValue) }
輸出結果爲:
I'm in lazy. lazyValue lazyValue
下面我們來解釋一下這段代碼,從而理解lazy委託的特點。在by關鍵字後跟上一個lazy+lambda表達式,那麼這個lazy是個啥:
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
actual關鍵字可以先忽略,它與多平臺相關。忽略後,可以看到這個lazy就是個普通的function,返回一個Lazy對象,那麼也就是說返回的這個Lazy對象作爲我們lazyValue屬性的委託對象。再看這個Lazy:
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
定義了一個擴展方法:getValue,這個getValue方法簽名對我們來說應該很熟悉了。同時我們還可以得出一個結論,對於委託類,除了以member的形式來定義getValue/setValue,還可以通過extension的方式來定義。
最後注意一下lazy函數的參數,一個不帶參數、返回一個T類型的lambda表達式(如果lambda表達式是function的最後一個參數,那麼推薦將其寫到外面)。這個lambda表達式的返回值就是我們要賦給屬性的值。
上面僅僅是對lazy委託本身進行了分析,那麼它有什麼特點呢?我們還得結合main方法中的測試代碼來看:
當第一次訪問lazyValue時,打印出了:
I'm in lazy. lazyValue.
當第二次訪問lazyValue時,僅打印了:
lazyValue.
可以看出,lazy後的lambda表達式只是在被委託屬性第一次被訪問的時候執行了一次,並且將返回值用來初始化了被委託屬性,之後對於被委託屬性的訪問,直接使用初始值。這裏說的訪問,確切地說是get()。
-
如果你對多線程編程敏感的話,可以隱約意識到,在多線程環境下,這裏會不會出現多線程同步的問題,畢竟lambda表達式裏不是原子操作。不慌,我們來看lazy的另一個重載方法:
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> = when (mode) { LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer) LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer) LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer) }
它會多一個參數,LazyThreadSafetyMode類型mode,LazyThreadSafetyMode實際上是一個枚舉類,它有三個枚舉對象:
LazyThreadSafetyMode.SYNCHRONIZED: 這種模式下,lambda表達式中的代碼是加了鎖的,確保只有一個線程能夠來執行初始化
LazyThreadSafetyMode.PUBLICATION: 這種模式下,lambda表達式中的代碼是允許多個線程同時訪問的,但是隻有第一個返回的值作爲初始值
LazyThreadSafetyMode.NONE: 這種模式下,lambda表達式中的代碼在多線程的情況下的行爲是不確定的,這種模式並不推薦使用,除非你確保它永遠不可能在多線程環境下使用,然後使用這種模式可以避免因鎖所帶來的額外開銷。很慶幸,默認情況下,lazy的模式是第一種,所以默認情況下是不會出現同步問題的。
5. 標準庫的屬性委託之:observable/vetoable
- 特點:可以對屬性值的變化進行監聽
-
示例代碼:
class Person { var name: String by Delegates.observable("<no name>") { property, oldValue, newValue -> println("property'name is ${property.name}, oldValue = $oldValue, newValue = $newValue") } } fun main() { val person = Person() person.name = "Alice" person.name = "Bob" }
我們關注by後面的部分就可以了,調用了Delegates.observable(),將它的返回值作爲委託對象:
public object Delegates { public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit): ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) { override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue) } }
Delegates是個對象,observable接收兩個參數:一個初始值,賦給被委託屬性;一個lambda表達式,lambda有三個回調參數,描述屬性的KProperty、舊值以及新值。一旦被委託屬性的值發生變化(即調用set方法)時,就會回調lambda表達式。
現在再來看main函數中的代碼就簡單多了:
person.name = "Alice" => 打印:
property'name is name, oldValue = <no name>, newValue = Alice
person.name = "Bob" => 打印:
property'name is name, oldValue = Alice, newValue = Bob
回過頭再來關注一下這個observable方法的返回值類型:ReadWriteProperty
/** * Base interface that can be used for implementing property delegates of read-write properties. * * This is provided only for convenience; you don't have to extend this interface * as long as your property delegate has methods with the same signatures. * * @param R the type of object which owns the delegated property. * @param T the type of the property value. */ public interface ReadWriteProperty<in R, T> { /** * Returns the value of the property for the given object. * @param thisRef the object for which the value is requested. * @param property the metadata for the property. * @return the property value. */ public operator fun getValue(thisRef: R, property: KProperty<*>): T /** * Sets the value of the property for the given object. * @param thisRef the object for which the value is requested. * @param property the metadata for the property. * @param value the value to set. */ public operator fun setValue(thisRef: R, property: KProperty<*>, value: T) }
在這個接口中,我們看到了有熟悉方法簽名的getValue、setValue方法。一起來讀一下這個接口的文檔註釋:
它是一個用來對可讀可寫(即var)的屬性實現屬性委託的接口;但是它的存在僅僅是爲了方便,只要我們的屬性委託擁有相同的方法簽名,開發者不必來繼承這個接口。
與之類似的還有個ReadOnlyProperty:
/** * Base interface that can be used for implementing property delegates of read-only properties. * * This is provided only for convenience; you don't have to extend this interface * as long as your property delegate has methods with the same signatures. * * @param R the type of object which owns the delegated property. * @param T the type of the property value. */ public interface ReadOnlyProperty<in R, out T> { /** * Returns the value of the property for the given object. * @param thisRef the object for which the value is requested. * @param property the metadata for the property. * @return the property value. */ public operator fun getValue(thisRef: R, property: KProperty<*>): T }
註釋基本同ReadWriteProperty類似,只不過它是服務於val屬性。
同observable委託有相同功能的還有一個:vetoable。
public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean): ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) { override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue) }
發現它的lambda會要求一個返回值,這個返回值有什麼作用呢?這與observable和vetoable的回調時機不同有關:observable的回調時機是在屬性值修改之後,vetoable的回調時機在屬性值被修改之前。如果返回值爲true,屬性值就會被修改成新值;如果返回值爲false,此次修改就會直接被丟棄。
我們來看示例代碼:
class Adult { var age: Int by Delegates.vetoable(18) { property, oldValue, newValue -> println("property'name is ${property.name}, oldValue = $oldValue, newValue = $newValue") newValue >= 18 } } fun main(){ val adult = Adult() adult.age = 25 println("adult.age = ${adult.age}") adult.age = 16 println("adult.age = ${adult.age}") }
當adult.age = 25時,屬性值被成功修改;adult.age = 16,修改操作被丟棄,修改失敗,屬性值還是原來的。
6. 對比一下lazy委託和observable委託:lazy委託專注於getValue(),observable委託專注於setValue()
7. map委託:
- 特點:對於屬性的訪問,直接委託給一個map對象。
- 要求:map的key要同屬性名保持一致。
-
對於val屬性:
class User(map: Map<String, Any?>) { val name: String by map val age: Int by map } fun main() { val user = User(mapOf( "name" to "David Lee", "age" to 25 )) println(user.name) //輸出 David Lee println(user.age) //輸出 25 }
-
對於var屬性:
class Student(map: MutableMap<String, Any?>) { var name: String by map var age: Int by map var address: String by map } fun main(){ val map: MutableMap<String, Any?> = mutableMapOf( "name" to "Alice", "age" to 23, "address" to "beijing" ) val student = Student(map) println(student.name) //Alice println(student.age) //23 println(student.address) //beijing println("---------------------------------") student.address = "hefei" println(student.address) // hefei println(map["address"]) // hefei }
對比可以得知:
- val的map委託的對象是Map<String, Any?>,var的map委託的對象MutableMap<String, Any?>
- 對於var屬性,對於MutableMap中的value的修改,會同步到屬性值;反之亦然。
8. 現在再回過頭反編譯一下我們自己動手實現的屬性委託:
getStr的反編譯結果就沒貼了,同setStr類似。通過這倆截圖,我們可以知道:kotlin編譯器爲RealSub類生成了兩個重要的部分:
- Delegate類型的實例成員:對於setStr()的實現邏輯,委託給Delegate類型的委託對象
- static final的KProperty[] $$delegatedProperties: 在static代碼塊中初始化,存儲被委託屬性的KProperty,然後在後續的setValue、getValue的調用中作爲其第二個參數。
9. 提供委託
終於寫到最後一個部分了,有點興奮也有點疲勞。前面我們介紹的屬性委託,我們介入的環節都是對於屬性的訪問,實際上我們還可以對於委託對象的生成(或者說選取)進行介入:
class People {
val name: String by DelegateProvider()
val address: String by DelegateProvider()
}
class DelegateProvider {
operator fun provideDelegate(thisRef: People, property: KProperty<*>): ReadOnlyProperty<People, String> {
println("I'm in provideDelegate.")
checkProperty()
return RealDelegate()
}
private fun checkProperty() {
val random = Random.Default
if (!random.nextBoolean()) {
throw RuntimeException("failed to create delegate.")
}
}
}
class RealDelegate : ReadOnlyProperty<People, String> {
override fun getValue(thisRef: People, property: KProperty<*>): String {
return "kotlin"
}
}
先撇開中間的DelegateProvider類不看,其他兩個類的實現符合我們之前介紹的理論。那麼中間的這個類有什麼特點或者說什麼要求呢?必須提供一個provideDelegate的方法,同樣地對於它的方法簽名是有要求的:
operator fun provideDelegate(thisRef: T, property: KProperty<*>): RealOnlyProperty<T, R>
或者
operator fun provideDelegate(thisRef: T, property: KProperty<*>): ReadWriteProperty<T, R>
再回到代碼實現中來,我們這裏通過checkProperty方法來模擬相關邏輯檢查,添加main方法進行測試:
fun main() {
val people = People()
println(people.name)
println(people.address)
}
然後看人品隨機,多運行幾次吧,肯定有不拋異常的時候。篇幅有點長,謝謝耐心閱讀!