NullPointException 利器 Kotlin 可选型

来源:互联网 发布:淘宝怎样隐藏订单 编辑:程序博客网 时间:2024/06/07 07:10

NullPointException (简称 NPE ) 被称作 The Billion Dollar Mistake 一直困扰着Java 和 Android 开发者。Kotlin 的类型系统中提供可选类型用于减少 NPE 问题带来的风险。

虽然,Kotlin 提供了可选类型用于减少 NPE 问题的风险,但是并没有办法完全消除 NPE 带来的隐患,本问将探讨如何巧妙地使用「可选型」更好的规避 NPE 的发生。

可选型定义

非空类型

我们先从可选型的定义开始,当我们在 Kotlin 中定义一个变量时,默认就是非空类型的,当你将一个非空类型置空的时候,编译器会告诉你这不可行。

var a: String = "abc"a = null // compilation error

因此,如果你后面任何时候使用该变量时,都可以放心的使用而不用担心会发生 NPE。所以要想远离 NPE,首先需要「尽可能的使用非空类型的定义」

可选型(可空类型)

虽然「非空类型」能够有效避免 NPE 的问题,但是有时候我们总不可避免的需要使用「可选类型」。在定义可选型的时候,我们只要在非空类型的后面添加一个 ? 就可以了。

var b: String? = "abc"b = null // ok

在使用可选型变量的时候,这个变量就有可能为空,所以在使用前我们应该对其进行空判断(在 Java 中我们经常这样做),这样往往带来带来大量的工作,这些空判断代码本身没有什么实际意义,并且让代码的可读性和简洁性带来了巨大的挑战。在网上可以看到许多人针对如何减少 NPE 提出了自己的建议,有的的确很不错,但成本依然很大。除此之外,还有一个最可恶的场景「我们会忘记」。

Kotlin 为了解决这个问题,它并不允许我们直接使用一个可选型的变量去调用方法或者属性。

val l = b.length // compilation error

你可以和 Java 中一样,在使用变量之前先进行空判断,然后再去调用。如果使用这种方法,那么空判断是必须的。

val l = if (b != null) b.length else -1

注意: 如果你定义的变量是全局变量,即使你做了空判断,依然不能使用变量去调用方法或者属性。这个时候你需要考虑使用下面的介绍的方法。

Kotlin 为可选型提供了一个安全调用操作符 ?.,使用该操作符可以方便调用可选型的方法或者属性。

val l = b?.length

这里 l 得到的返回依然是一个可选型 Int?

Kotlin 还提供了一个强转的操作符 !!,这个操作符能够强行调用变量的方法或者属性,而不管这个变量是否为空,如果这个时候该变量为空时,那么就会发生 NPE。所以如果不想继续陷入 NPE 的困境无法自拔,请不要该操作符走的太近。

Elvis 操作符

上面有提到一种情况,当 b 为空时,返回它的长度值给一个默认值 -1。要实现这样的逻辑当然可以用 ifelse 的逻辑判断实现,但 Kotlin 提供了一个更优雅的书写方式?:

val l = b?.length ?: -1

b?.length ?: -1if (b != null) b.length else -1 完全等价的。

其实你还可以在 ?: 后面添加任何表达式,比如你可以在后面会用 returnthrow(在 Kotlin 中它们都是表达式)。

fun foo(node: Node): String? {  val parent = node.getParent() ?: return null  val name = node.getName() ?: throw IllegalArgumentException("name expected")  // ...}

let 函数

let 是官方 stdlib 提供的标准函数库里面的函数,这个函数巧妙的利用的 Kotlin 语言的特性让let 接受的表达式参数中的调用方是非空的。

val listWithNulls: List<String?> = listOf("A", null)for (item in listWithNulls) {    item?.let { println(it) } // prints A and ignores null}

上面代码的只会输出 A,而不会输出 null

需要注意的是,这个方法调用的时候必须要使用 ?. 操作符调用才能生效哦。如果你的部分代码依赖于一个可选型变量为非空的时候,就可以使用let 函数。

参考这个函数的实现,下面我尝试提供几个自己定义的方法。

自定义处理

这里定义的两个方法是参考 Swift 里面的 if letguard 进行的抽象。

orElse 函数

orElse 是和 Elvis 函数结合使用的,默认 Elvis 后面只能直接或者执行一个表达式获取返回值或者直接通过return 或者 throw 结束当前函数的执行。结合 orElse 函数,你能够更加灵活的处理前面的null

  • 你可以处理一些逻辑以后,再返回一个可用的值。
var a:String? = nullvar b = a ?: orElse {    // 做任何事   return@orElse "s"}
  • 也可以处理一些逻辑后, 通过return 或者 throw 结束当前函数的执行。
var a:String? = nullvar b = a ?: orElse {    // 做任何事   return}

guard 函数

Elvis 默认只能对单个变量或表达式是否为空进行处理,当碰到多个变量需要一起判断时,就会束手无策,guard 就是为了解决这个问题。

fun testGuard(a: String?, b: String?, c: String?){    guard(a, b, c) ?: orElse {        print("a or b or c is null ")        return    }    // 现在 `a`,`b`,`c` 都是不为空}

由于没有编译器的支持,所以暂时还不能实现 空屏蔽。

这里定义的两个函数的实现,你可以自己尝试去实现一下,就当是个练习(鬼笑)。AndroidExtension有具体的实现代码。

总结

经过一系列分析以后,我们已经对怎么使用好 Kotlin 可选型有一定的了解,如果不想 NPE 问题不断困扰,可以参考这里总结的几条。

  • 尽可能的使用非空类型的定义
  • 远离 !!,如果非要用,请调用代码在前面「三行之内」进行非空判断
  • 熟练使用 Elvis 操作符
  • 自定义一些常用的函数,让自己的代码更流畅

参考资料

  • null-safety
  • trailing-closures-in-guard