空安全是Kotlin特性之一,本文以一个常见的“错误”推导场景为例来分析 ?. 的实现。

使用kotlin开发时可能大家都遇到过这种情况:

1
2
3
4
5
6
private var user: User? = null
fun main() {
if (user != null) {
user.hello()
}
}

IDE 会提示 user.hello() 这一行语法错误,我们必须显式使用 !! 或者 ?. 来解决,也就是说 IDE 认为 user 变量依然可能为null。“怎么可能,我明明已经在外层手动判断了 user 不为 null ”,这可能是我们首先想到的,然后开始巴拉这个「智能推导」实际是智障。

这其实并不是推导算法出现了 BUG,而是 user 确实无法被推导,原因就在于 user 作为一个共享变量,任意时刻都存在被其它线程修改的可能性,也就是说即使前面进行了 null 判断,也不能保证在执行 hello 方法时的这一刻 user 一定不为 null,因此这种情况需要我们手动处理。如果我们确定 user 只会被一个线程操作,那么可以使用 !! 来明确告知以通过编译,否则应选择 ?. 保证user字段的安全调用。

?. 的实现

但是这里又会产生一个新疑问:我们知道 ?. 其实就是针对类似 if(x != null) {x.xx} 这样的语法而提炼的糖,既然 user 任何时候都可能为 null,为什么语法糖 ?. 能严格保证不会出现空指针异常?万千疑惑不如从反编译后的 Java 文件寻找答案:

1
2
3
4
5
6
public final void main() {
User var10000 = this.user;
if (var10000 != null) {
var10000.hello();
}
}

以上是对 user?.hello() 的 de-sugar,我们可以清楚地看到其背后实际的逻辑:通过定义一个临时局部变量指向 user 变量对应的对象,然后后续的判断和执行都是调用的这个局部变量。
定义临时局部变量来避免一些并发问题的操作非常常见,由于局部变量只属于被定义时的线程环境所有,其它线程无法对其进行修改,var10000 是否为 null 只取决于定义后赋值的这一刻 user 的值,因此后续无论 user 的指向如何变化,都不会再影响变量 var10000 的值,只要这个局部变量不为 null,调用 hello 方法时肯定不会导致空指针异常。

结尾

到此这个由推导引出的疑问算是有了最终答案。现在我们回顾在此之前的解决方式,可以明显发现是存在潜在问题的:我们在判断 user 不为 null 之后直接使用 user!!.hello() 依然存在空指针异常的可能性。因此最后再次强调:仅当我们确认所操作的变量一定安全,否则对 !! 要慎重使用。