NullPointException 利器 Kotlin 可选型 | Ohmer's Blog
tags: Kotlin
NullPointException (简称 NPE ) 被称作 The Billion Dollar Mistake 一直困扰着Java 和 Android 开发者。Kotlin 的类型系统中提供可选类型用于减少 NPE 问题带来的风险。
虽然,Kotlin 提供了可选类型用于减少 NPE 问题的风险,但是并没有办法完全消除 NPE 带来的隐患,本问将探讨如何巧妙地使用「可选型」更好的规避 NPE 的发生。
可选型定义
非空类型
我们先从可选型的定义开始,当我们在 Kotlin 中定义一个变量时,默认就是非空类型的,当你将一个非空类型置空的时候,编译器会告诉你这不可行。
1 2 | var a: String = "abc" a = null // compilation error |
因此,如果你后面任何时候使用该变量时,都可以放心的使用而不用担心会发生 NPE。所以要想远离 NPE,首先需要「尽可能的使用非空类型的定义」。
可选型(可空类型)
虽然「非空类型」能够有效避免 NPE 的问题,但是有时候我们总不可避免的需要使用「可选类型」。在定义可选型的时候,我们只要在非空类型的后面添加一个 ? 就可以了。
1 2 | var b: String? = "abc" b = null // ok |
在使用可选型变量的时候,这个变量就有可能为空,所以在使用前我们应该对其进行空判断(在 Java 中我们经常这样做),这样往往带来带来大量的工作,这些空判断代码本身没有什么实际意义,并且让代码的可读性和简洁性带来了巨大的挑战。在网上可以看到许多人针对如何减少 NPE 提出了自己的建议,有的的确很不错,但成本依然很大。除此之外,还有一个最可恶的场景「我们会忘记」。
Kotlin 为了解决这个问题,它并不允许我们直接使用一个可选型的变量去调用方法或者属性。
1 | val l = b.length // compilation error |
你可以和 Java 中一样,在使用变量之前先进行空判断,然后再去调用。如果使用这种方法,那么空判断是必须的。
1 | val l = if (b != null) b.length else -1 |
注意: 如果你定义的变量是全局变量,即使你做了空判断,依然不能使用变量去调用方法或者属性。这个时候你需要考虑使用下面的介绍的方法。
Kotlin 为可选型提供了一个安全调用操作符 ?.,使用该操作符可以方便调用可选型的方法或者属性。
1 | val l = b?.length |
这里 l 得到的返回依然是一个可选型 Int?。
Kotlin 还提供了一个强转的操作符 !!,这个操作符能够强行调用变量的方法或者属性,而不管这个变量是否为空,如果这个时候该变量为空时,那么就会发生 NPE。所以如果不想继续陷入 NPE 的困境无法自拔,请不要该操作符走的太近。
Elvis 操作符
上面有提到一种情况,当 b 为空时,返回它的长度值给一个默认值 -1。要实现这样的逻辑当然可以用 ifelse 的逻辑判断实现,但 Kotlin 提供了一个更优雅的书写方式 ?:。
1 | val l = b?.length ?: -1 |
b?.length ?: -1 和 if (b != null) b.length else -1 完全等价的。
其实你还可以在 ?: 后面添加任何表达式,比如你可以在后面会用 return 和 throw(在 Kotlin 中它们都是表达式)。
1 2 3 4 5 | fun foo(node: Node): String? { val parent = node.getParent() ?: return null val name = node.getName() ?: throw IllegalArgumentException("name expected") // ... } |
let 函数
let 是官方 stdlib 提供的标准函数库里面的函数,这个函数巧妙的利用的 Kotlin 语言的特性让 let 接受的表达式参数中的调用方是非空的。
1 2 3 4 | 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 let 和 guard 进行的抽象。
orElse 函数
orElse 是和 Elvis 函数结合使用的,默认 Elvis 后面只能直接或者执行一个表达式获取返回值或者直接通过 return 或者 throw 结束当前函数的执行。结合 orElse 函数,你能够更加灵活的处理前面的 null。
- 你可以处理一些逻辑以后,再返回一个可用的值。
1 2 3 4 5 | var a:String? = null var b = a ?: orElse { // 做任何事 return@orElse "s" } |
- 也可以处理一些逻辑后, 通过
return或者throw结束当前函数的执行。
1 2 3 4 5 | var a:String? = null var b = a ?: orElse { // 做任何事 return } |
guard 函数
Elvis 默认只能对单个变量或表达式是否为空进行处理,当碰到多个变量需要一起判断时,就会束手无策,guard 就是为了解决这个问题。
1 2 3 4 5 6 7 8 | 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操作符 - 自定义一些常用的函数,让自己的代码更流畅