Kotlin-强大的委托机制

2,299 阅读2分钟

第十五讲 Kotlin之强大的委托机制

前介

委托是 Kotlin 中新引入的一种概念,它能给我们开发中带来很多的遍历,接下来由我给大家揭开它的面纱,并且给大家提供几种好用的实战经验。

多继承

Java 的世界中是只能单继承的。在 Kotlin 的世界中是存在多继承的(我在前面好像说过)。

什么?多继承?Kotlin 的产物不是 class 吗?那就必定要受限于 Java 的语言的约定啊?那就不应该存在多继承的概念啊?

其实 Kotlin 的确不存在多继承,但是存在一种委托机制,可以达到类似多继承的关系,我们俗称曲线救国。

要实现通过委托来完成多继承,委托对象必须实现一个接口。


/**
 * 定义人类
 */
open class People(val name: String) {
    fun eat() {
        println("$name  chifan ")
    }

    fun sleep() {
        println("$name  sleep")
    }
}

/**
 * 定义程序员接口
 */

interface ICode {
    fun codeing()
}

/**
 * 可以写代码技能
 */
class Code(val name: String) : ICode {
    override fun codeing() {
        println("$name  写代码")
    }
}


/**
 * 可以看到,我通过接口+by 关键词 来进行了一次委托
 */
class CodePeople(name: String) : People(name), ICode by Code(name)

fun main() {
    /**
     * 创建 CodePeople 对象
     */

    val codePeople = CodePeople("阿文")

    /**
     * 调用吃饭睡觉方法
     */
    codePeople.eat()
    codePeople.sleep()

    /**
     * 调用委托过来的方法
     */
    codePeople.codeing()
}

通过上面的代码,我们发现 codePeople 对象既能调用父类 People 的方法,也能调用到委托的 Codecodeing 方法,有那么一点多继承的感觉。哪我们能重写 codeing 方法吗?

class CodePeople(name: String) : People(name), ICode by Code(name){
    override fun codeing() {
        println("$name  写牛逼的代码")
    }
}

发现是可以重写 codeing 方法了,越来越像多继承的感觉了。

多继承的本质

Java 是不可能存在多继承的。Kotlin 是在 Java 的基础上建立的帝国,所以也不能丢了根本,哪它是如何实现的呢?我们看下反编译的 Java 代码。

public final class CodePeople extends People implements ICode {
   // $FF: synthetic field
   private final Code ?delegate_0;

   public CodePeople(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super(name);
      // 可以注意到这里,当我们构造 CodePeople 对象的时候,创建了一个 Code 对象
      this.?delegate_0 = new Code(name);
   }

   // 实现了接口方法,并且调用了 Code 的 codeing 方法
   public void codeing() {

      this.?delegate_0.codeing();
   }
}

原来,如此简单,其实就是一个 静态代理模式。这下知道为啥,所有的委托都需要实现一个接口了吧。那么也就是说委托的对象,只能调用接口的委托对象方法。

/**
 * 定义程序员接口
 */

interface ICode {
    fun codeing()
}

/**
 * 可以写代码技能
 */
class Code(val name: String) : ICode {
    override fun codeing() {
        println("$name  写代码")
    }

    /**
     * 我们多增加一个方法,但是不在接口中
     */
    fun zhuangB(){
        println("$name  开始装B")
    }
}


/**
 * 可以看到,我通过接口+by 关键词 来进行了一次委托
 */
class CodePeople(name: String) : People(name), ICode by Code(name)
fun main() {
    /**
     * 创建 CodePeople 对象
     */

    var codePeople = CodePeople("阿文")
    /**
     * 能调用 装B 方法吗? 编译器报错
     */
    codePeople.zhuangB()
}

通过上面的代码,我们验证了理论,其实多继承就是 静态代理模式 ,所以说只能调用接口中的方法,不能调用非接口的方法。例如 Code 类的 zhuangB 方法 CodePeople 对象是无权调用的。

委托变量

还记的 val 变量 和 var 变量的区别吗?我们复习下,其实 Kotlin 定义的变量都隐藏着方法。

val userToken:String
    get() {
      return  "阿文"
    }

var userAge:Int = 0
    get() {
        return 18
    }
    set(value) {
        field = value + 2
    }

我相信大家,应该了解这些变量的 getset 方法的作用,我这里就不叙述啦!若忘记了,请翻阅我的变量篇章。

有啥用呢?这里有很大的学问,例如我一个遍历的来源和设置都是通过一个文件(例如:Andorid 中的 SharedPreferences 或数据库),我们可以尝试重写隐藏的 getset 方法。

当然这种方案是不建议的,我们可以通过委托来解决这个问题,Kotlin 为我们提供了一个委托类。 ReadWritePropertyReadOnlyProperty 接口类。

实战,通过 ReadWriteProperty 来实现,获取用户的 Token 的获取和保存。

object User{
    /**
     * 例如 定义 userToken
     * 它的来源都是在一个文件中,注意我这里使用了委托,将 userToken 的 get 和 set 房委托给了 TokenProperty 对象
     */
    var userToken: String by TokenProperty()
}


/**
 * 定义委托的对象
 */
class TokenProperty : ReadWriteProperty<User, String> {
    override fun getValue(thisRef: User, property: KProperty<*>): String {
        // todo 可以从文件读取token
        val token = loadFileToekn()
        return token
    }

    override fun setValue(thisRef: User, property: KProperty<*>, value: String) {
        // todo 将 token 保存到文件
        saveToken(value)
    }

    private fun saveToken(value: String) {


    }

    private fun loadFileToekn():String{
        // 模拟从文件读取 token
        return "阿文"
    }
}

fun main() {
    /**
     * 如果我们登陆成功了,我们要设置 Token 直接使用
     *
     * 根据委托规则,他会调用 TokenProperty 对象的 setValue 方法
     */
    User.userToken = "阿文"

    /**
     * 若想获取 直接获取
     *
     * 根据委托规则,他会调用 TokenProperty 对象的 getValue 方法
     */
    val toekn = User.userToken

    print(toekn)
}

通过上面的例子,我们应该看到了将变量委托的好处了吧?我们可以直接通过调用变量或赋值的方式,增加我们一些其他的逻辑。我这个例子委托的是 var 变量,如果我们想委托的是 val 变量,我们只需要实现 ReadOnlyProperty 接口。
当然委托接口方法还有 thisRefproperty 大家可以自行查阅,是啥东西,其实很简单。

通过委托来完美实现 AndoridSharedPreferences 封装代码 (参考)[wenyingzhi.com/mu-lu-2/kot…]。

懒加载

实战中,我们经常遇到懒加载。例如:一个对象在用的时候再去创建,但是用的地点有很多,我们以前的逻辑都是在使用的地方加 null 判断,如为 null 就创建对象。这种脑壳疼的代码弄的我

我想 Kotlin 的设计师也为此烦恼吧?因此通过委托的机制,为我们提供了一个懒加载的方法 lazy

/**
 * 需要懒加载的对象
 *
 * 懒加载的对象必须是 val 变量?大伙想想为啥?
 */
val lazyObj by lazy {
   Random()
}

fun main() {
    /**
     * 当我使用的时候才会去创建 lazyObj 对象
     */
    val nextInt = lazyObj.nextInt()
    print(nextInt)
}

看看是不是超级简单啊?只需要写 by lazy ,就能很简单的完成一个懒加载对象,并且再也不用在使用的地方加 null 判断了。

lazy 的原理呢?其实就是一个委托。

妈的我记错了?我刚去看了下源码 by lazy 返回的是一个 Lazy 对象。我的天,我一直以为是实现了 ReadOnlyProperty 接口呢?

真相

新机呲挖一呲冒黑套呲!!

我最终看看了下 ReadWriteProperty 就是一个接口,那么 Kotlinby 关键词到底是在干嘛呢?我们看下 ReadWriteProperty 定义。

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)
}

可以发现 setValuegetValue 方法是通过 operator 重载操作符的。我们知道 by 关键词最终会调用者2个方法。也就是说使用 by 关键词,只需要重载 getValuesetValue 方法即可。
哪我们看下 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 还体贴的考虑到了,线程安全问题。接下来我们去追寻 SynchronizedLazyImpl 实现。

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    // 有一个 value 变量
    override val value: T
        get() {
            // 这里面的内容,主要是判断当前懒加载的对象是否加载,如果加载了就直接返回
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

通过上面的代码,我们发现奇怪没有重载 getValuesetValue 方法啊?最后我自己转念一想 SynchronizedLazyImpl 中存在一个 value 变量,不就默认存在 getValuesetValue 方法吗?也就是说这个 value 变量是关键,并且名字必须是 value

我们尝试自定义一个懒加载。

class Code {
    fun eat(food: String = "米饭") {
        synchronized(this) {
            println("吃$food")
        }
    }
}

val c by myLazyCall {
    Code()
}

fun <T> myLazyCall(block: () -> T) = MyLazy<T>(block)

class MyLazy<out T>(val block: () -> T) :Lazy<T>{
    val tmp: T? = null


    override fun isInitialized(): Boolean {
        return tmp != null
    }

    override val value: T
        get() {
            // 判断是否初始化,如果未初始化,调用 lambda 创建返回
            return if (!isInitialized()) {
                block()
            } else {
                tmp!!
            }
        }
}

可以注意到,我们定义的委托必须实现 Lazy<T> 接口,并且重写 value 变量。

补充

其实 Kotlin 委托还为我们提供了很多好用的方法,大部分都在 Delegates 类中。
实战可以参考:委托实现双击 back 退出