Kotlin 变量的空安全 (Null Safety)
前言
这篇文章巩固 Kotlin 变量的空安全特性。
本文大纲
前面介绍过 Koltin 声明类型的语法,这里将介绍 Kotlin 类型系统里最重要的特性:空安全(Void Safety/Null Safety)。在 Kotlin 中,不可为空的变量和可为空的变量被强行分开了(Java
有 @Nullable
和 @NonNull
注解) Kotlin 为什么要这样设计呢?带着这个疑问往下看:
1. 场景分析
某天你正在优雅的编写新业务代码,leader 突然来告诉你,有一个线上的空指针 crash,赶紧处理一下。赶紧 git stash
了自己的代码,切换到出问题的那个类。
这是一个管理音频播放的类,叫 PlayerController,用来播放用户上传的 ugc 音频内容。播放是一个很基础通用的功能,所以这个类依赖了一个播放库 AudioPlayer,PlayerController 主要是实现业务功能。
之前的维护者刚离职,你现在临时接任,对里面的结构是不太熟悉的。这个类年代久远,在某个初期版本就上线了,承载了无数的业务变更。里面代码逻辑混乱,业务和通用代码耦合在了一起。你想过重构,但功能实在太多了,需要很长的时间,且现在功能也比较稳定了,重构的收益对业务增长没有明显帮助 那还是先打个补丁吧。
来看看代码:
PlayerController.java:
这是个很典型的依赖了底层组件的封装类。初始化,释放,播放,暂停这些是外部接口。里面还包含着很多空判断和 proxy
的代码。这样写代码量就迅速起来了。
这个类在后面讲解很多 Kotlin 特性的场景都会用它,可以先熟悉一下
开始 crash
分析。通过上报的报错信息,发现是 mAudioPlayer.stop()
这行空指针错误。mAudioPlayer 在init()
时被赋值,release()
时被释放,且为了防止内存泄漏被设置为 null。并且考虑到并发操作,即 mAudioPlayer 这个变量在任何使用的时候都可能为 null。
但外部已经有空条件判断了,且这是最新的版本才暴露的问题,为什么会这样呢?
通过 代码提交记录排查后了解到,是mAudioPlayer.stop()
之前新增了一些业务代码,而新增代码有耗时操作。这导致在空判断时非空,但进入 if 代码块之后,线程被切换了,上层调用了release()
,等线程再切回来的时候 mAudioPlayer 已经变成 null ,再执行就出现了空指针错误。
最简单的解决办法就是给mAudioPlayer.stop()
单独再包一层*。虽然很丑,但很管用,大伙也很喜欢用,特别是灰度测试时不允许大幅改动代码。
或者是给所有 mAudioPlayer 操作都加上锁 synchronized。不过考虑到里面 API 有耗时操作,这样有可能会造成 UI 卡顿。
不加锁的话也有多次调用,即破坏幂等性的风险。
总之事情就这样暂时解决了。代码随着时间的迁移,越来越多变量可能为空的地方加上了if (xxx != null)
的保护代码,甚至可能一个类 10% 的行都是空指针保护!涉及到逻辑冗长的地方,空保护的嵌套甚至到达了 5 层以上! 我直接好家伙。。
但这确实是 Java
的最通用解决办法。那么 Kotlin
怎么更优雅的解决这个问题呢?
2. Kotlin 非空类型/可空类型(NonNull/Nullable)声明
之前提到:在 Kotlin
中,不可为空的变量和可为空的变量被强行分开了。那么是如何分开的呢?很简单,默认的类型声明不能为空,类型后面跟问号"?"即表示可为空。
看下面这段代码:
观察 string1,string2
可以得出:
当像 Java
那样声明一个 String
对象时,在之后的赋值也是不能被赋值为空的。意味着如果一个变量的类型为 String,则在任何时候都不可能为空。
观察 string3
可以得出:
声明对象为 String?
类型,可以将其设置为空。典型场景:初始化这个变量的时候,还暂时无法得到其值,就必须用可空类型的声明方法了。
观察 string4,string5
可以得出:
类型推断是完全根据初始化时的赋值来确定的。不会根据后面的赋值作为依据来推断这个变量的类型。所以需要像 string3
那样显式声明为 String?
。
3. Kotlin 可空(Nullable)类型的调用
声明一个非空变量,意味着你可以随意的调用他的方法而不用担心空指针错误,相对应的,可空变量则无法保证。Kotlin
通过不允许可空变量直接调用方法来保证不会出现空指针错误。可空变量应该怎么调用呢?
Kotlin
中可空变量的调用方式是:调用的"."号前加"?"或"!!"。前者表示是,如果非空则调用,否则不调用;后者表示是,如果非空则调用,否则抛出 Exception。来看例子:
生产环境不建议使用双叹号!!
,一般只用于测试环境。使用双叹号!!
可以理解为放弃 Kotlin
的空安全特性。
4. Kotlin 可空(Nullable)的传递性
如果可空对象调用了方法,因为方法有可能不被执行,那么如果接收它的返回值,那么返回值的类型应该是什么呢?继续使用上面A
这个类,来看看这个例子:
可以看到,本来getMyCode()
方法返回的是 Int 类型,但由于 a4 为可空类型,所以 myCode 被编译器认为是 Int? 类型。所以,可空是具有传递性的。
!!
由于在变量为空时会抛出异常,所以返回值就还是为 Int,若抛异常的话,后面的代码就不会被执行了。
a4 要写在外面的原因是,若声明为局部变量,即使 a4 被声明为 A?,但由于局部变量的关系,编译器会把 myCode 纠正为 Int,而不是 Int?
用链式调用的话,就会变成如下这样:
看起来比较不好看懂。但不用担心,Kotlin
有其他的特性来协助你处理可空变量,不用写出像这样不好理解的代码。
5. 回到之前场景
用 Kotlin 来实现代码,只需要将 mAudioPlayer 声明为可空类型就可以:
Kotlin 写起来简洁很多,而且减少了嵌套层数。
版权声明: 本文为 InfoQ 作者【子不语Any】的原创文章。
原文链接:【http://xie.infoq.cn/article/9bdc49e228ca2ae2ee6b013f5】。文章转载请联系作者。
评论