Kotlin 中延迟初始化的非空属性,如何避免使用不必要的 !! 操作符

96
作者 JohnnyShieh
2017.06.27 11:49* 字数 1276

博客原文 http://johnnyshieh.me/posts/kotlin-property-lazy-init-not-null/

最近在写 Kotlin 版本的 Gank 客户端(干货集中营 app)时,发现一个非常烦人的事情:有的成员属性不能在构造函数中初始化,会在稍后某的地方完成初始化,可以确定是非空,但是因为不能在构造时初始化只能定义为可能为空的类型(T?),然后在后面调用时都要加上!!操作符。下面本文将逐步分析这种场景的解决方案,最终提供一种优雅的方式。

这里先给出最终解决方式(为了部分喜欢直奔主题的开发者):

  • notNull 委托属性

  • lateinit 修饰符

注:本文的第一种解决方法来源于《Kotlin For Android Developers》,学习 Kotlin 的朋友有兴趣可以看看。

问题场景

相信肯定有很多开发者也遇到一样的场景,因为这种情况在 Android 中很常见:在 Activity、Fragment、Service... 中经常有些属性只能在onCreate中才能完成初始化,而且之后不会再修改可以确定为非空,如下面代码所示:

class App : Application() {

    companion object {
        var instance: App? = null   // kotlin 中的单例
    }

    var okHttpClient: OkHttpClient? = null   // 使用 Dagger 2 注入
        @Inject set

    override fun onCreate() {
        super.onCreate()

        instance = this
        // 使用 Dagger 2 注入 okHttpClient 实例,完成初始化
        ...
    }
}

上面代码中instanceokHttpClient都只能在onCreate函数中完成初始化,但是之后都是可以确定是非空,但是在后面调用只能通过instance!!okHttpClient!!的方式调用,感觉非常变扭。

转化为非空属性

为了解决每次都要加上!!操作符的问题,最简单的方法就是增加一个返回非 null 值的函数。

class App : Application() {

    companion object {
        private var instance: App? = null   // kotlin 中的单例
        fun instance() = instance!!
    }

    var okHttpClient: OkHttpClient? = null   // 使用 Dagger 2 注入
        @Inject set

    fun okHttpClient() = okHttpClient!!

    override fun onCreate() {
        super.onCreate()

        instance = this
        // 使用 Dagger 2 注入 okHttpClient 实例,完成初始化
    }
}

但是这种方式有点不自然,不能直接调用那个属性,只有通过另外一个函数返回那个属性。有没有其他方法可以达到类似的效果呢?

委托属性

Kotlin 中的委托属性可以实现类似的效果,把一个属性的值委托给一个类,当使用属性的get或者set的时候,实际上调用的属性所委托的那个类的getValuesetValue函数。

属性委托的结构如下:

class Delegate<T> : ReadWriteProperty<Any?, T> {    // T 就是委托属性的类型
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {   // thisRef 是拥有委托属性的类的引用,property 是委托属性的元数据
        return ...
    }
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {    // value 是被设置的值
        return ...
    }
}

如果这个属性是 val 类型的,就需要继承ReadOnlyProperty<Any?, T>,就只有一个getValue函数。跟多关于委托属性的内容,请看官方文档 Delegated Properties

notNull 委托

所以可以利用委托属性来返回非空属性,而且标准委托属性的notNull委托正好适用于这种场景(可惜官方文档中关于委托属性的介绍中没有介绍它)。

class App : Application() {

    companion object {
        var instance: App by Delegates.notNull()
    }

    var okHttpClient: OkHttpClient by Delegates.notNull()   // 使用 Dagger 2 注入
        @Inject set

    override fun onCreate() {
        super.onCreate()

        instance = this
        // 使用 Dagger 2 注入 okHttpClient 实例,完成初始化
    }
}

使用notNull委托还可以不用把属性声明为可能为空的类型,非常适合只能延迟初始化的属性,那么它的原理是什么,下面是它的源码:

public object Delegates {
    public fun <T: Any> notNull(): ReadWriteProperty<Any?, T> = NotNullVar()
    // Delegates.notNull() 返回的其实是 NotNullVar 委托
    ...
}

private class NotNullVar<T: Any>() : ReadWriteProperty<Any?, T> {
    private var value: T? = null    // 持有属性的值

    public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")  // 如果属性的值为空,就会抛出异常
    }

    public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        this.value = value
    }
}

NotNullVar的实现可以看出,它的getValue函数返回非空的属性,和一开始提到的额外定义的函数的作用是一样,但是使用Delegates.notNull()不用声明额外的函数,而且可以直接把属性声明为非空类型。

自定义委托

上面的情况其实还有一个问题,因为使用Delegates.notNull()的属性必须是var的,这意味可以任意修改这个值,有没有什么办法让属性只能被赋值一次,第二次赋值就会抛异常呢?

只需要修改上面的NotNullVarsetValue函数就可以,看下面自定义的NotNullSingleInitVar委托:

private class NotNullSingleInitVar<T: Any>() : ReadWriteProperty<Any?, T> {
    private var value: T? = null    // 持有属性的值

    public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")  // 如果属性的值为空,就会抛出异常
    }

    public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        if (null == this.value) this.value = value
        else throw IllegalStateException("Property ${property.name} already initialized")   // 第二次赋值就抛出异常
    }
}

再使用扩展函数添加到Delegates中:

fun <T: Any> Delegates.notNullSingleInit(): ReadWriteProperty<Any?, T> = NotNullSingleInitVar()

接下来就可以使用Delegates.notNullSingleInit()了。

所以平常我们可以使用Delegates.notNull()来委托需要延迟初始化的非空属性,如果不想初始化的值被修改,还可以使用上面的Delegates.notNullSingleInit()(需要把上面相关的声明加入项目中)。

lateinit 修饰符

除了使用委托属性返回非空类型外,有没有一种方式直接告诉编译器,这个属性需要延迟初始化,不会为空呢?Kotlin 为此提供了lateinit修饰符:

class App : Application() {

    companion object {
        lateinit var instance: App
    }

    lateinit var okHttpClient: OkHttpClient  // 使用 Dagger 2 注入
        @Inject set

    override fun onCreate() {
        super.onCreate()

        instance = this
        // 使用 Dagger 2 注入 okHttpClient 实例,完成初始化
    }
}

在初始化之前,访问lateinit属性会抛出异常,和notNull委托一样。

注意:lateinit修饰符所修饰的属性必须是非空类型,而且不能是原生类型(Int、Float、Char等),而且该修饰符只能用于类体中,不能在主构造函数中,也不能修饰局部变量。而委托属性可以使用于原生类型和局部变量中。

总结

  • 一般情况使用lateinit修饰符,最为优雅。

  • 当类型是原生类型,或者为局部变量时,只能只用notNull委托。

推荐阅读:

  • 《Kotlin For Android Developers》

想看更多精彩内容,欢迎关注我的公众号 JohnnyShieh,每周一准时更新!


Kotlin