[译]如何在你的Kotlin代码中移除所有的!!(非空断言)

翻译说明:

原标题: 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种方法去做到这一点:

1) 使用val替代var

Kotlin让你在语言的层面去考虑不变性,这点看起来很不错。 val 是只读,var 是可变。建议你尽可能多的使用只读属性。因为他们是线程安全的并且在函数式编程方面效果很好。如果你使用它们时当做是不可变的,那么你就不必关心可空性了,但是只要注意val实际上是可变的。

2) 使用lateinit

有时候你不能使用不变属性。例如,在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>()

3)使用let函数

这里有个Kotlin中很常见的编译时错误:

[译]如何在你的Kotlin代码中移除所有的!!(非空断言)_第1张图片

令我恼火的是:我知道这个可变属性在空类型检查后不能被改变。很多开发人员通过以下方式快速修复它:

private var mPhotoUrl: String? = null

fun uploadClicked() {
    if (mPhotoUrl != null) {
        uploadPhoto(mPhotoUrl!!)
    }
}

但是这里有个优雅解决办法,那就是使用 let函数

private var mPhotoUrl: String? = null

    fun uploadClicked() {
        mPhotoUrl?.let { uploadPhoto(it) }
}

4)创建全局的函数去处理更多复杂的case

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)
   }
}

5)使用Elvis操作符

Elvis操作符作用不错在于如果你有空类型情况出现,会有返回值的功能。比如以下代码:

fun getUserName(): String {
   if (mUserName != null) {
       return mUserName!!
   } else {
       return "Anonymous"
   }
}

可以被替代如下代码:

fun getUserName(): String {
   return mUserName ?: "Anonymous"
}

6)按照你自己的声明崩溃

尽管你知道类型是可空的,但是有些情况下你知道一些属性是不可能为空的。一旦为空了,你应该很容易知道这是一个bug.然而抛弃使用 !! 非空断言,系统就会给你抛出一个很难去debug的通用的常 KotlinNullPointerException。使用内置函数 requireNotNull 或者 checkNotNull 和一些附带的异常消息易于调试。类似如下代码:

uploadPhoto(intent.getStringExtra("PHOTO_URL")!!)

以上代码可以替代为:

uploadPhoto(requireNotNull(intent.getStringExtra("PHOTO_URL"), { "Activity parameter 'PHOTO_URL' is missing" }))

总结

如果你按照这6个提示,你可以从你的Kotlin代码删除所有的 !! 非空断言。这样你的代码将更安全,更可调试,更清洁。

译者有话说

  • 1、我为什么要翻译这篇博客?

我们知道Kotlin中一个非常好的特性就是空类型安全的特性,也就是极大程度上避免了像Java中的空指针问题。是不是表示我使用Kotlin就不会存在空指针了呢。可以这么说Kotlin空类型安全特性,对于会使用的人来说将会是非常方便和安全,对于不会使用的人来说(特别是一些初学者,包括还在用Java语言思想写Kotlin代码的人)空类型安全特性的代码会写得非常的ugly,例如译文中反面教材例子滥用 !!非空断言。可能不仅仅是代码丑陋的问题,还很容易带来KotlinNullPointException. 如果你还在滥用!!(非空断言)处理Kotlin中空类型的话,看完本篇博客不妨尝试一种优雅的方式去实现空类型安全

  • 2、核心点提炼以及使用中需要注意的问题

第一对于尽量多的使用val替代var这个建议,我在之前博客中多次提到。它可以避免出现一些不必要错误以及很好支持函数式编程。

第二就是关于使用lateinit的问题,需要特别补充一点,当你在使用lateinit的时候,一定要保证你使用的这个属性,必须要在它初始化之后使用。而且在开发中一个坑就是接收网络请求返回成功后的数据属性不要用lateinit的修饰,因为由于某种异常情况,你的网络请求失败,无法回调到成功callback中,此时你的属性没有被初始化,而代码执行到使用这个属性时候就会抛出上面所说的UninitializedPropertyAccessException异常。建议使用lateinit属性时,你非常清楚改属性初始化是在使用之前,比如一般在onCreate方法中初始化的一些属性就可以声明成lateinit.

  • 3、总结

关于空类型安全问题,其实还有很多需要注意的点,后续会有专门专题博客来阐述Kotlin中空类型安全的问题。而这篇译文则是先认识一下,以及在实际开发中如何优雅实现空类型安全的特性。

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不定期翻译一篇Kotlin国外技术文章。如果你也喜欢Kotlin,欢迎加入我们~~~

Kotlin系列文章,欢迎查看:

Kotlin邂逅设计模式系列:

  • 当Kotlin完美邂逅设计模式之单例模式(一)

数据结构与算法系列:

  • 每周一算法之二分查找(Kotlin描述)

翻译系列:

  • [译] Kotlin中关于Companion Object的那些事
  • [译]记一次Kotlin官方文档翻译的PR(内联类)
  • [译]Kotlin中内联类的自动装箱和高性能探索(二)
  • [译]Kotlin中内联类(inline class)完全解析(一)
  • [译]Kotlin的独门秘籍Reified实化类型参数(上篇)
  • [译]Kotlin泛型中何时该用类型形参约束?
  • [译] 一个简单方式教你记住Kotlin的形参和实参
  • [译]Kotlin中是应该定义函数还是定义属性?
  • [译]如何在你的Kotlin代码中移除所有的!!(非空断言)
  • [译]掌握Kotlin中的标准库函数: run、with、let、also和apply
  • [译]有关Kotlin类型别名(typealias)你需要知道的一切
  • [译]Kotlin中是应该使用序列(Sequences)还是集合(Lists)?
  • [译]Kotlin中的龟(List)兔(Sequence)赛跑

原创系列:

  • 教你如何完全解析Kotlin中的类型系统
  • 如何让你的回调更具Kotlin风味
  • Jetbrains开发者日见闻(三)之Kotlin1.3新特性(inline class篇)
  • JetBrains开发者日见闻(二)之Kotlin1.3的新特性(Contract契约与协程篇)
  • JetBrains开发者日见闻(一)之Kotlin/Native 尝鲜篇
  • 教你如何攻克Kotlin中泛型型变的难点(实践篇)
  • 教你如何攻克Kotlin中泛型型变的难点(下篇)
  • 教你如何攻克Kotlin中泛型型变的难点(上篇)
  • Kotlin的独门秘籍Reified实化类型参数(下篇)
  • 有关Kotlin属性代理你需要知道的一切
  • 浅谈Kotlin中的Sequences源码解析
  • 浅谈Kotlin中集合和函数式API完全解析-上篇
  • 浅谈Kotlin语法篇之lambda编译成字节码过程完全解析
  • 浅谈Kotlin语法篇之Lambda表达式完全解析
  • 浅谈Kotlin语法篇之扩展函数
  • 浅谈Kotlin语法篇之顶层函数、中缀调用、解构声明
  • 浅谈Kotlin语法篇之如何让函数更好地调用
  • 浅谈Kotlin语法篇之变量和常量
  • 浅谈Kotlin语法篇之基础语法

Effective Kotlin翻译系列

  • [译]Effective Kotlin系列之考虑使用原始类型的数组优化性能(五)
  • [译]Effective Kotlin系列之使用Sequence来优化集合的操作(四)
  • [译]Effective Kotlin系列之探索高阶函数中inline修饰符(三)
  • [译]Effective Kotlin系列之遇到多个构造器参数要考虑使用构建器(二)
  • [译]Effective Kotlin系列之考虑使用静态工厂方法替代构造器(一)

实战系列:

  • 用Kotlin撸一个图片压缩插件ImageSlimming-导学篇(一)
  • 用Kotlin撸一个图片压缩插件-插件基础篇(二)
  • 用Kotlin撸一个图片压缩插件-实战篇(三)
  • 浅谈Kotlin实战篇之自定义View图片圆角简单应用

你可能感兴趣的:(Kotlin,Kotlin,android,空类型安全,!!,非空断言)