kotlin 消除!!强制非空

背景

kotlin官网对!!的描述:

!!操作符是为 NPE 爱好者准备的:非空断言运算符(!!)将任何值转换为非空类型,若该值为空则抛出异常。我们可以写 b!! ,这会返回一个非空的 b 值 (例如:在我们例子中的 String)或者如果 b 为空,就会抛出一个 NPE 异常:

val l = b!!.length

因此,如果你想要一个 NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。

使用!!意味着我们允许程序抛出NPE异常,但是一般NPE异常意味着App的崩溃退出,是应该避免抛出的异常,所以我们需要消除 !! 强制非空。

前置知识

var,val与非空

var str1: String? = ""
val str2: String? = "" 
fun doSth() {
    handle(str1) //编译器会提示str1是可空的 handle(str2) //str2的值已经确定,实际类型为String 
}

fun handle(s: String) { }

为什么我判断了str != null,仍然需要加!!

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方法可空

    1. LiveData.value可空

解决

  • 对于String和List我们一般给定一个非空默认值,但要确保该非空默认值不会被误解(如不合适,采用对象的处理方式)。一般String="",List=emptyList()

  • 对于对象成员变量

    • 对于第一种具有生命周期类的场景,延迟初始化时使用apply,后续使用中通过?.let等辅助函数。对于只初始化一次的变量可以考虑使用lateinit或by lazy{}

    • 对于数据模型类的场景

      • 首先应当确认字段只被赋值一次,如果只赋值一次,可以考虑将字段变为val类型。

      • 确认该字段是否必须可空,可以考虑赋予非空默认值,或设置不可空。

  • LiveData.value可空,推荐在调用函数内部入手,允许参数可空,另外再考虑其他通用解决办法。

案例:List成员变量

    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)

    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)
}

案例:使用(?.let) 还是 (?: 或orEmpty() 赋予默认值)

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造成的块复杂度增加

案例:可空Int或Boolean比较

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) {
    handle(list)
}
 
if (!list1.isNullOrEmpty()) {
    handleDataList(list1!!)
} if (!list2.isNullOrEmpty()) {
    handleDataList(list2!!)
} if (!list3.isNullOrEmpty()) {
    handleDataList(list3!!)
}

解决1:从函数外部考虑

fun handleDataList(list: List) {
    handle(list)
}

handleDataList(list1.orEmpty()) 
handleDataList (list2.orEmpty())
handleDataList (list3.orEmpty())

解决2:从函数内部考虑

fun handleDataList(list: List?) {
    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可空类型,在每次使用时判空。

你可能感兴趣的:(android,kotlin,java)