首發于公眾號: DSGtalk1989
16.委托
-
委托實現
一般直接使用
: Class by c
來做委托實現,大致的意思就是class Derived(b: Base) : Base by b fun main() { val b = BaseImpl(10) Derived(b).print() }
有很多類都實現了或者繼承了
Base
,具體Derived
要如何去實現Base
的抽象方法,不單獨定義,直接委托給b
,也就是說Derived
的抽象方法實現就是去調b
的抽象方法,即上面的BaseImpl
方法print
-
屬性委托
屬性一般委托給重載操作符
getValue
和setValue
的類,此處我們先不要過多的去在意什么是重載操作符,我們只要記得操作符的重載寫法一般是operator fun
。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.") } }
對于上述的
getValue
和setValue
方法,我們可能單獨去記的話特別痛苦,騎士只需要實現兩個接口即可。public interface ReadOnlyProperty<in R, out T> { public operator fun getValue(thisRef: R, property: KProperty<*>): T } public interface ReadWriteProperty<in R, T> { public operator fun getValue(thisRef: R, property: KProperty<*>): T public operator fun setValue(thisRef: R, property: KProperty<*>, value: T) }
這種做法可以讓你在設置或者獲取屬性的屬性做一些自己額外的處理。
val a : String by Delegate()
-
映射(map)代理
直接通過
map
定義的方式,來給類進行屬性賦值。class User(val 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 ))
-
延遲屬性lazy
很多時候我們會需要數據只初始化一次,其他的時候我們直接拿來用就OK,這個時候就需要用到延遲屬性。
val lazyValue: String by lazy { println("computed!") "Hello" }
類似于這一類的
lambda
,最終取得都是最后一行的結果,所以這個lazy
代理的就是String
屬性,我們在取lazyValue
的時候,會打印一次computed!
,之后每一次都只是取到值Hello
-
lazy的延遲模式
這里需要說明一點,延遲屬性的在初始化的過程中默認是會上鎖的,也就是說如果有多個線程同時去調延遲屬性的話,會出現其他線程在初始化過程中無法調用的情況。
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer) 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) }
即我們可以在
lazy
后面傳入具體的mode
方法,從上面的代碼中我們可以看到默認的是LazyThreadSafetyMode.SYNCHRONIZED
,還有另外兩個LazyThreadSafetyMode.PUBLICATION
,和LazyThreadSafetyMode.NONE
,我們來逐一看一下他們的注解說明.public enum class LazyThreadSafetyMode { /** * Locks are used to ensure that only a single thread can initialize the [Lazy] instance. */ SYNCHRONIZED, /** * Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value, * but only the first returned value will be used as the value of [Lazy] instance. */ PUBLICATION, /** * No locks are used to synchronize an access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined. * * This mode should not be used unless the [Lazy] instance is guaranteed never to be initialized from more than one thread. */ NONE, }
-
SYNCHRONIZED
首先是默認的
SYNCHRONIZED
,上鎖為了保證只有一條線程可去初始化lazy
屬性。也就是說同時多線程進行訪問該延遲屬性時,一旦沒有初始化好,其他線程將無法訪問。
fun main() {
val lazyValue: String by lazy{
println("start" + Thread.currentThread().name)Thread.sleep(5000) println("end" + Thread.currentThread().name) "Hello" + Thread.currentThread().name } Thread { println(lazyValue) }.start() Thread { println(lazyValue) }.start() println(lazyValue)
}
控制臺輸出的是 ```js startThread-0 endThread-0 HelloThread-0 HelloThread-0 HelloThread-0
即第一個線程先進行初始化,其他線程都被堵塞,直到第一個線程完成初始化之后得到了
lazyValue
,其他線程才可以拿來用。 -
-
PUBLICATION
再來看第二個,解釋的意思是對于還沒有被初始化的
lazy
對象,初始化的方法可以被不同的線程調用很多次,直到有一個線程初始化先完成,那么其他的線程都將使用這個初始化完成的值。我們將上面的
lazy
改成lazy(LazyThreadSafetyMode.PUBLICATION)
來看一下控制臺輸出結果。
startThread-0
startThread-1
startmain
endThread-0
endmain
endThread-1
HelloThread-0
HelloThread-0
HelloThread-0
```
我們能看到三個線程同時開始了初始化,但是線程`Thread-0`先完成了初始化,得到了延遲屬性的值為`HelloThread-0`,所以后面兩個緊接著完成的線程都使用這個值。
-
NONE
最后一個,不會對任何訪問和初始化上鎖,也就是說完全放任,官方是這么描述的
如果你確定初始化將總是發生在單個線程,那么你可以使用LazyThreadSafetyMode.NONE 模式, 它不會有任何線程安全的保證以及相關的開銷
主要就是為了節省開銷,因為他是線程不安全的。
我們將上面的
lazy
改成lazy(LazyThreadSafetyMode.NONE)
來看一下控制臺輸出結果。startThread-0 startmain startThread-1 endThread-0 endmain endThread-1 Hellomain HelloThread-0 HelloThread-1
能夠看到各個線程都產生了各自的結果,也就是說自己玩自己的,明明是
Thread-0
先完成,但是最先出來的結果是Hellomain
,所以將變得完全不可控。但是我們嘗試著在代碼最后再加一個延遲展示Thread.sleep(2000) println(lazyValue)
打印出來為
HelloThread-1
可以發現,最終的結果將以最后一次為準。也就是說雖然線程是不安全的,但是一旦經過這一團混亂的初始化后,最后完成初始化的那個線程得到的結果將是最終的結果。此后將以這個結果為準。
-
局部委托屬性
這個是kotlin
1.1
之后開始引入的委托方式。即我們在方法內部可以直接委托屬性。上面我們說到
lazy
的使用方式是后面直接跟lambda
表達式,在kotlin中方法可以直接當成參數進行傳遞,一旦需要直接將方法跟在后面我們就用lambda
的方式。舉個例子val initFun = { "init" } val memoizedFoo1 by lazy(initFun) val memoizedFoo2 by lazy{ "init" }
上面的兩種
lazy
使用方式實際上是一樣的。在kotlin中,如果傳參中最后一個參數是方法,則可以放到參數括號外通過
lambda
給到。如果只有一個函數參數的話,可以省去圓括號,直接用大括號的
lambda
表達式官方使用了比較繞的
lazy
方式來進行局部屬性委托class Foo { fun isValid(): Boolean { return Random().nextBoolean() } fun doSomething() { println("doSomething") } } fun example(computeFoo: () -> Foo) { val memoizedFoo by lazy(computeFoo) //memoizedFoo: Foo if (memoizedFoo.isValid()) { memoizedFoo.doSomething() } }
example
方法需要傳入一個無參的有數據返回的方法,這個方法正好符合上面說的lazy
方法要求,因此我們在example
的方法中生成一個屬性,并且委托給這個computeFoo
,也就是說這個方法computeFoo
只要跑完一遍就不會再跑了,即memoizedFoo
只要確定指向了一個地址,就不會再變了。當我們之后再反復調example方法的時候,memoizedFoo
都不會變了。
Kotlin學習筆記之 13 基礎操作符run、with、let、also、apply