博客原文 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 实例,完成初始化
...
}
}
上面代码中instance
和okHttpClient
都只能在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
的时候,实际上调用的属性所委托的那个类的getValue
和setValue
函数。
属性委托的结构如下:
class Delegate : ReadWriteProperty { // 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
,就只有一个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 notNull(): ReadWriteProperty = NotNullVar()
// Delegates.notNull() 返回的其实是 NotNullVar 委托
...
}
private class NotNullVar() : ReadWriteProperty {
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
的,这意味可以任意修改这个值,有没有什么办法让属性只能被赋值一次,第二次赋值就会抛异常呢?
只需要修改上面的NotNullVar
的setValue
函数就可以,看下面自定义的NotNullSingleInitVar
委托:
private class NotNullSingleInitVar() : ReadWriteProperty {
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 Delegates.notNullSingleInit(): ReadWriteProperty = 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,每周一准时更新!