翻译说明:
原标题: How to remove all !! from your Kotlin code
原文地址: https://android.jlelse.eu/how-to-remove-all-from-your-kotlin-code-87dc2c9767fb
原文作者: David Vávra
空安全特性是Kotlin语言最好语法特性之一。它让你在语言层面来考虑可空性,以致于你可以避免很多在Java中常见的隐藏空指针异常。然而当你通过工具自动将Java代码转化成Kotlin时,你会发现有很多的 !!(非空断言) 标记出现。按道理在你的代码中不应该有任何的 !!(非空断言) 出现,除非它是一个快速原型。并且我相信这是对的,因为 !!(非空断言) 的出现基本上的意味着 “你这里有可能存在未处理的KotlinNullPointerException”.
Kotlin有一些智能的机制去避免这些空指针的问题,但是弄明白它并不是那么直接和容易。这里有6种方法去做到这一点:
Kotlin让你在语言的层面去考虑不变性,这点看起来很不错。 val 是只读,var 是可变。建议你尽可能多的使用只读属性。因为他们是线程安全的并且在函数式编程方面效果很好。如果你使用它们时当做是不可变的,那么你就不必关心可空性了,但是只要注意val实际上是可变的。
有时候你不能使用不变属性。例如,在Android中onCreate方法被调用时,一些属性被初始化。在这些场景中,Kotlin有个语言特性叫做 lateinit
使用!!的代码:
private var mAdapter: RecyclerAdapter<Transaction>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mAdapter = RecyclerAdapter(R.layout.item_transaction)
}
fun updateTransactions() {
mAdapter!!.notifyDataSetChanged()
}
用下面代码替代上面代码:
private lateinit var mAdapter: RecyclerAdapter<Transaction>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mAdapter = RecyclerAdapter(R.layout.item_transaction)
}
fun updateTransactions() {
mAdapter.notifyDataSetChanged()
}
需要注意的是,访问未初始化的 lateinit 修饰的属性会抛出UninitializedPropertyAccessException异常。
很遗憾的是lateinit 不支持基本数据类型,例如Int. 针对基本数据类型实现方式你可以使用委托(delegate)类似以下实现:
private var mNumber: Int by Delegates.notNull<Int>()
这里有个Kotlin中很常见的编译时错误:
令我恼火的是:我知道这个可变属性在空类型检查后不能被改变。很多开发人员通过以下方式快速修复它:
private var mPhotoUrl: String? = null
fun uploadClicked() {
if (mPhotoUrl != null) {
uploadPhoto(mPhotoUrl!!)
}
}
但是这里有个优雅解决办法,那就是使用 let函数
private var mPhotoUrl: String? = null
fun uploadClicked() {
mPhotoUrl?.let { uploadPhoto(it) }
}
let 函数是一个很好的简单检查空类型替代方式,但是可能会出现更多复杂的cases,例如:
if (mUserName != null && mPhotoUrl != null) {
uploadPhoto(mUserName!!, mPhotoUrl!!)
}
你可以使用两个let函数嵌套调用,但是那样可读性很差。在Kotlin中你可以全局访问函数,因此你可以轻松地创建你所需要的函数。类似如下方法:
ifNotNull(mUserName, mPhotoUrl) {
userName, photoUrl ->
uploadPhoto(userName, photoUrl)
}
这个函数定义代码:
fun <T1, T2> ifNotNull(value1: T1?, value2: T2?, bothNotNull: (T1, T2) -> (Unit)) {
if (value1 != null && value2 != null) {
bothNotNull(value1, value2)
}
}
Elvis操作符作用不错在于如果你有空类型情况出现,会有返回值的功能。比如以下代码:
fun getUserName(): String {
if (mUserName != null) {
return mUserName!!
} else {
return "Anonymous"
}
}
可以被替代如下代码:
fun getUserName(): String {
return mUserName ?: "Anonymous"
}
尽管你知道类型是可空的,但是有些情况下你知道一些属性是不可能为空的。一旦为空了,你应该很容易知道这是一个bug.然而抛弃使用 !! 非空断言,系统就会给你抛出一个很难去debug的通用的常 KotlinNullPointerException。使用内置函数 requireNotNull 或者 checkNotNull 和一些附带的异常消息易于调试。类似如下代码:
uploadPhoto(intent.getStringExtra("PHOTO_URL")!!)
以上代码可以替代为:
uploadPhoto(requireNotNull(intent.getStringExtra("PHOTO_URL"), { "Activity parameter 'PHOTO_URL' is missing" }))
如果你按照这6个提示,你可以从你的Kotlin代码删除所有的 !! 非空断言。这样你的代码将更安全,更可调试,更清洁。
我们知道Kotlin中一个非常好的特性就是空类型安全的特性,也就是极大程度上避免了像Java中的空指针问题。是不是表示我使用Kotlin就不会存在空指针了呢。可以这么说Kotlin空类型安全特性,对于会使用的人来说将会是非常方便和安全,对于不会使用的人来说(特别是一些初学者,包括还在用Java语言思想写Kotlin代码的人)空类型安全特性的代码会写得非常的ugly,例如译文中反面教材例子滥用 !!非空断言。可能不仅仅是代码丑陋的问题,还很容易带来KotlinNullPointException. 如果你还在滥用!!(非空断言)处理Kotlin中空类型的话,看完本篇博客不妨尝试一种优雅的方式去实现空类型安全
第一对于尽量多的使用val替代var这个建议,我在之前博客中多次提到。它可以避免出现一些不必要错误以及很好支持函数式编程。
第二就是关于使用lateinit的问题,需要特别补充一点,当你在使用lateinit的时候,一定要保证你使用的这个属性,必须要在它初始化之后使用。而且在开发中一个坑就是接收网络请求返回成功后的数据属性不要用lateinit的修饰,因为由于某种异常情况,你的网络请求失败,无法回调到成功callback中,此时你的属性没有被初始化,而代码执行到使用这个属性时候就会抛出上面所说的UninitializedPropertyAccessException异常。建议使用lateinit属性时,你非常清楚改属性初始化是在使用之前,比如一般在onCreate方法中初始化的一些属性就可以声明成lateinit.
关于空类型安全问题,其实还有很多需要注意的点,后续会有专门专题博客来阐述Kotlin中空类型安全的问题。而这篇译文则是先认识一下,以及在实际开发中如何优雅实现空类型安全的特性。
欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不定期翻译一篇Kotlin国外技术文章。如果你也喜欢Kotlin,欢迎加入我们~~~
Kotlin邂逅设计模式系列:
数据结构与算法系列:
翻译系列:
原创系列:
Effective Kotlin翻译系列
实战系列: