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

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

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

  • notNull 委托属性

  • lateinit 修饰符

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

问题场景

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 值的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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函数。

属性委托的结构如下:

1
2
3
4
5
6
7
8
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委托正好适用于这种场景(可惜官方文档中关于委托属性的介绍中没有介绍它)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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委托还可以不用把属性声明为可能为空的类型,非常适合只能延迟初始化的属性,那么它的原理是什么,下面是它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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委托:

1
2
3
4
5
6
7
8
9
10
11
12
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中:

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

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

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

lateinit 修饰符

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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》
END
Johnny Shieh wechat
我的公众号,不只有技术,还有咖啡和彩蛋!