Kotlin学习笔记之 16 委托

186 阅读6分钟

首发于公众号: 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

  • 属性委托

    属性一般委托给重载操作符getValuesetValue的类,此处我们先不要过多的去在意什么是重载操作符,我们只要记得操作符的重载写法一般是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.")
          }
      }
    

    对于上述的getValuesetValue方法,我们可能单独去记的话特别痛苦,骑士只需要实现两个接口即可。

    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)`来看一下控制台输出结果。
    
    ```js
    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都不会变了。