最近在写 Kotlin 版本的 Gank 客户端(干货集中营 app)时,发现一个非常烦人的事情:有的成员属性不能在构造函数中初始化,会在稍后某的地方完成初始化,可以确定是非空,但是因为不能在构造时初始化只能定义为可能为空的类型(T?),然后在后面调用时都要加上!!
操作符。下面本文将逐步分析这种场景的解决方案,最终提供一种优雅的方式。
这里先给出最终解决方式(为了部分喜欢直奔主题的开发者):
notNull 委托属性
lateinit 修饰符
注:本文的第一种解决方法来源于《Kotlin For Android Developers》,学习 Kotlin 的朋友有兴趣可以看看。
问题场景
相信肯定有很多开发者也遇到一样的场景,因为这种情况在 Android 中很常见:在 Activity、Fragment、Service… 中经常有些属性只能在onCreate
中才能完成初始化,而且之后不会再修改可以确定为非空,如下面代码所示:
|
|
上面代码中instance
和okHttpClient
都只能在onCreate
函数中完成初始化,但是之后都是可以确定是非空,但是在后面调用只能通过instance!!
和okHttpClient!!
的方式调用,感觉非常变扭。
转化为非空属性
为了解决每次都要加上!!
操作符的问题,最简单的方法就是增加一个返回非 null 值的函数。
|
|
但是这种方式有点不自然,不能直接调用那个属性,只有通过另外一个函数返回那个属性。有没有其他方法可以达到类似的效果呢?
委托属性
Kotlin 中的委托属性可以实现类似的效果,把一个属性的值委托给一个类,当使用属性的get
或者set
的时候,实际上调用的属性所委托的那个类的getValue
和setValue
函数。
属性委托的结构如下:
|
|
如果这个属性是 val 类型的,就需要继承ReadOnlyProperty<Any?, T>
,就只有一个getValue
函数。跟多关于委托属性的内容,请看官方文档 Delegated Properties。
notNull 委托
所以可以利用委托属性来返回非空属性,而且标准委托属性的notNull
委托正好适用于这种场景(可惜官方文档中关于委托属性的介绍中没有介绍它)。
|
|
使用notNull
委托还可以不用把属性声明为可能为空的类型,非常适合只能延迟初始化的属性,那么它的原理是什么,下面是它的源码:
|
|
从NotNullVar
的实现可以看出,它的getValue
函数返回非空的属性,和一开始提到的额外定义的函数的作用是一样,但是使用Delegates.notNull()
不用声明额外的函数,而且可以直接把属性声明为非空类型。
自定义委托
上面的情况其实还有一个问题,因为使用Delegates.notNull()
的属性必须是var
的,这意味可以任意修改这个值,有没有什么办法让属性只能被赋值一次,第二次赋值就会抛异常呢?
只需要修改上面的NotNullVar
的setValue
函数就可以,看下面自定义的NotNullSingleInitVar
委托:
|
|
再使用扩展函数添加到Delegates
中:
|
|
接下来就可以使用Delegates.notNullSingleInit()
了。
所以平常我们可以使用Delegates.notNull()
来委托需要延迟初始化的非空属性,如果不想初始化的值被修改,还可以使用上面的Delegates.notNullSingleInit()
(需要把上面相关的声明加入项目中)。
lateinit 修饰符
除了使用委托属性返回非空类型外,有没有一种方式直接告诉编译器,这个属性需要延迟初始化,不会为空呢?Kotlin 为此提供了lateinit
修饰符:
|
|
在初始化之前,访问lateinit
属性会抛出异常,和notNull
委托一样。
注意:lateinit
修饰符所修饰的属性必须是非空类型,而且不能是原生类型(Int、Float、Char等),而且该修饰符只能用于类体中,不能在主构造函数中,也不能修饰局部变量。而委托属性可以使用于原生类型和局部变量中。
总结
一般情况使用
lateinit
修饰符,最为优雅。当类型是原生类型,或者为局部变量时,只能只用
notNull
委托。
推荐阅读:
- 《Kotlin For Android Developers》