kotlin官网对!!的描述:
!!操作符是为 NPE 爱好者准备的:非空断言运算符(
!!
)将任何值转换为非空类型,若该值为空则抛出异常。我们可以写b!!
,这会返回一个非空的b
值 (例如:在我们例子中的String
)或者如果b
为空,就会抛出一个NPE
异常:val l = b!!.length
因此,如果你想要一个 NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。
使用!!意味着我们允许程序抛出NPE异常,但是一般NPE异常意味着App的崩溃退出,是应该避免抛出的异常,所以我们需要消除 !! 强制非空。
- var str1: String? = ""
- val str2: String? = ""
- fun doSth() {
- handle(str1) //编译器会提示str1是可空的 handle(str2) //str2的值已经确定,实际类型为String
- }
-
- fun handle(s: String) { }
- var str: String?
- fun doSth() {
- if (str != null) {
- handle(str!!) // 不加!!编译器会报错
- }
- }
-
- fun handle(s: String) { }
在单线程条件下,判断了str != null,自然可以将str作为String类型,不会出现问题。
但是在多线程条件下,可能str的值可能会在handle(str)调用前被更改,这种情况下str仍可能为null。
注:可以将str改造成val变量,就不会出现上述问题。
在历史代码问题解决的经验中发现,绝大多数使用!!的case来源于类成员变量(>95%),因为许多变量设置为了可空类型,从而在整个类范围内使用都需要判空。
从数据结构的角度来看可空的类成员变量主要包括:
String
List
对象
常见的场景主要包括:
Activity,Fragment,View,Adapter等具有生命周期的类中,许多变量需要等到特定生命周期才初始化,需要延迟初始化或多次被赋值。
数据模型类(data class),如一般考虑到请求响应结果的不确定性,很多字段提供了可空类型,多数情况下字段只被赋值一次。
调用Java方法可空
LiveData.value可空
解决:
对于String和List我们一般给定一个非空默认值,但要确保该非空默认值不会被误解(如不合适,采用对象的处理方式)。一般String="",List=emptyList()
对于对象成员变量
对于第一种具有生命周期类的场景,延迟初始化时使用apply,后续使用中通过?.let等辅助函数。对于只初始化一次的变量可以考虑使用lateinit或by lazy{}
对于数据模型类的场景
首先应当确认字段只被赋值一次,如果只赋值一次,可以考虑将字段变为val类型。
确认该字段是否必须可空,可以考虑赋予非空默认值,或设置不可空。
LiveData.value可空,推荐在调用函数内部入手,允许参数可空,另外再考虑其他通用解决办法。
- private var mMessages: List
? = null - override fun initView() {
- arguments?.let {
- mMessages = it.getStringArrayList("messages")
- }
- if (mMessages != null) {
- for (i in mMessages!!.indices) {
- val messageView = getMessageView(
- i,
- mMessages!![i]
- )
- mBinding.llMessageContainer.addView(messageView)
- }
- }
- }
解决:通过给List一个非空的默认值
- private var mMessages: List
= emptyList() - override fun initView() {
- arguments?.let {
- mMessages = it.getStringArrayList("messages").orEmpty()
- }
- for (i in mMessages.indices) {
- val messageView = getMessageView(
- i,
- mMessages[i]
- )
- mBinding.llMessageContainer.addView(messageView)
- }
- }
注意:需考虑初始赋值的emptyList()对后边的代码逻辑无影响
- private var mMediaPlayer: MediaPlayer? = null
- private var mPlaybackInfoListener: VideoPlaybackListener? = null //初始化
- private fun initMediaPlayer() {
- if (mMediaPlayer == null) {
- mMediaPlayer = MediaPlayer()
- mMediaPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC)
- mMediaPlayer!!.setOnPreparedListener { mp: MediaPlayer? ->
- if (mPlaybackInfoListener != null) {
- mPlaybackInfoListener!!.onStateChanged(PlayState.PREPARED)
- mPlaybackInfoListener!!.onDurationChanged(mMediaPlayer!!.duration)
- }
- }
- }
- }
-
- //后续使用
- override fun play(videoUrl: String) {
- if (mMediaPlayer != null && !mMediaPlayer!!.isPlaying && videoUrl == currentPlayingUrl) {
- mMediaPlayer!!.start()
- if (mPlaybackInfoListener != null) {
- mPlaybackInfoListener!!.onStateChanged(PlayState.PLAYING)
- }
- updateVideoProgress()
- }
- }
解决:延迟初始化时使用apply,后续使用中通过?.let等辅助函数
- private var mMediaPlayer: MediaPlayer? = null
- private var mPlaybackInfoListener: VideoPlaybackListener? = null
-
- //初始化
- private fun initMediaPlayer() {
- if (mMediaPlayer == null) {
- mMediaPlayer = MediaPlayer().apply {
- setAudioStreamType(AudioManager.STREAM_MUSIC)
- setOnPreparedListener {
- mPlaybackInfoListener?.onStateChanged(PlayState.PREPARED)
- mPlaybackInfoListener?.onDurationChanged(duration)
- }
- }
- }
- }
-
- //后续使用
- override fun play(videoUrl: String) {
- mMediaPlayer?.let {
- if (it.isPlaying && videoUrl == currentPlayingUrl) {
- it.start()
- mPlaybackInfoListener?.onStateChanged(PlayState.PLAYING)
- updateVideoProgress()
- }
- }
- }
- data class UserModel {
- var name: String?,
- var nick: String?,
- var tags: List
?, - var address: Address?,
- }
解决:优先考虑val,其次赋予默认值
- data class UserModel {
- val name: String,
- val nick: String?,
- val tags: List
= emptyList(), - val address: Address?,
- }
- // case1
- if (mStateBarView != null) {
- mStateBarView!!.setBackgroundColor(color)
- }
- // case2
- if (mLoadingDialog != null && mLoadingDialog!!.isLoadingShowing) {
- return
- }
- if (activity != null) {
- mLoadingDialog = LoadingView()
- mLoadingDialog!!.showLoadingView(activity!!, loadingStyle)
- }
解决:这种问题一般是由于?.和!!.不了解导致
- mStateBarView?.setBackgroundColor(color)
-
- if (mLoadingDialog?.isLoadingShowing == true) return
- activity?.let {
- mLoadingDialog = LoadingView()
- mLoadingDialog?.showLoadingView(it, loadingStyle)
- }
- val str1 = entity.str1!!
- useStr1(str1)
解决1: 使用let,如果entity.str1为空则不执行useStr1(str1)
- entity.str1?.let {
- useStr1(it)
- }
解决2:使用?: 或orEmpty() 赋予默认值,无论entity.str1是否为空,都执行useStr1(str1)
- val str1 = entity.str1.orEmpty()
- // 或者
- val str1 = entity.str1 ?: "" useStr1(str1)
小技巧:两种方式都可行时,使用?: 或orEmpty() 赋予默认值,可以避免多个let造成的块复杂度增加
- if (viewModel.selectedMedia.value!!.size >= maxCount) {
- ToastUtil.showToast("")
- } if (dataModel.isVip!!) {
- //
- } if (dataModel.likeCount!! >= 0) {
- //
- } if (dataModel.likeCount!! == 0) {
- //
- }
解决:
- // 通过orEmpty() API提供默认值
- if (viewModel.selectedMedia.value.orEmpty().size >= maxCount) {
- ToastUtil.showToast("")
- }
- // 显式指定true
- if (dataModel.isVip == true) {
- }
- // 看情况是否使用(会影响代码可读性),使用默认值-1来表达整体条件空默认值的false
- if (dataModel.likeCount ?: -1 >= 0) {
- }
- // ==是equals的重载,左边可空(疑问)
- if (dataModel.likeCount == 0) {
- }
- fun handleDataList(list: List<DataModel>) {
- handle(list)
- }
-
- if (!list1.isNullOrEmpty()) {
- handleDataList(list1!!)
- } if (!list2.isNullOrEmpty()) {
- handleDataList(list2!!)
- } if (!list3.isNullOrEmpty()) {
- handleDataList(list3!!)
- }
解决1:从函数外部考虑
- fun handleDataList(list: List<DataModel>) {
- handle(list)
- }
-
- handleDataList(list1.orEmpty())
- handleDataList (list2.orEmpty())
- handleDataList (list3.orEmpty())
解决2:从函数内部考虑
- fun handleDataList(list: List<DataModel>?) {
- if (list.isNullOrEmpty()) return
- handle(list)
- }
-
- handleDataList(list1)
- handleDataList(list2)
- handleDataList(list3)
小技巧:一般带else条件的判空很难使用let改造,可以从内部方法中着手改造(或者赋予默认值)
- // 考虑从handleDataList内部改造或赋予默认值
- if (model != null) {
- handleDataList(model.list!!)
- handleBoolean(model.flag!!)
- } else {
- Toast.makeText("");
- }
案例:
- private val saleViewModel by lazy { ViewModelProvider(parentFragment!!)[SaleViewModel::class.java] }
-
- private val appInfo = IAppInfoProvider::class.impl()!!
目前如要解决此问题,只能让这类变量为val可空类型,在每次使用时判空。