• kotlin 消除强制非空!!


    背景

    kotlin官网对!!的描述:

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

    val l = b!!.length

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

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

    前置知识

    var,val与非空

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

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

    1. var str: String?
    2. fun doSth() {
    3. if (str != null) {
    4. handle(str!!) // 不加!!编译器会报错
    5. }
    6. }
    7. 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成员变量

    1. private var mMessages: List? = null
    2. override fun initView() {
    3. arguments?.let {
    4. mMessages = it.getStringArrayList("messages")
    5. }
    6. if (mMessages != null) {
    7. for (i in mMessages!!.indices) {
    8. val messageView = getMessageView(
    9. i,
    10. mMessages!![i]
    11. )
    12. mBinding.llMessageContainer.addView(messageView)
    13. }
    14. }
    15. }

    解决:通过给List一个非空的默认值

    1. private var mMessages: List = emptyList()
    2. override fun initView() {
    3. arguments?.let {
    4. mMessages = it.getStringArrayList("messages").orEmpty()
    5. }
    6. for (i in mMessages.indices) {
    7. val messageView = getMessageView(
    8. i,
    9. mMessages[i]
    10. )
    11. mBinding.llMessageContainer.addView(messageView)
    12. }
    13. }

    注意:需考虑初始赋值的emptyList()对后边的代码逻辑无影响

    案例:对象成员变量

    1. private var mMediaPlayer: MediaPlayer? = null
    2. private var mPlaybackInfoListener: VideoPlaybackListener? = null //初始化
    3. private fun initMediaPlayer() {
    4. if (mMediaPlayer == null) {
    5. mMediaPlayer = MediaPlayer()
    6. mMediaPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC)
    7. mMediaPlayer!!.setOnPreparedListener { mp: MediaPlayer? ->
    8. if (mPlaybackInfoListener != null) {
    9. mPlaybackInfoListener!!.onStateChanged(PlayState.PREPARED)
    10. mPlaybackInfoListener!!.onDurationChanged(mMediaPlayer!!.duration)
    11. }
    12. }
    13. }
    14. }
    15. //后续使用
    16. override fun play(videoUrl: String) {
    17. if (mMediaPlayer != null && !mMediaPlayer!!.isPlaying && videoUrl == currentPlayingUrl) {
    18. mMediaPlayer!!.start()
    19. if (mPlaybackInfoListener != null) {
    20. mPlaybackInfoListener!!.onStateChanged(PlayState.PLAYING)
    21. }
    22. updateVideoProgress()
    23. }
    24. }

    解决:延迟初始化时使用apply,后续使用中通过?.let等辅助函数

    1. private var mMediaPlayer: MediaPlayer? = null
    2. private var mPlaybackInfoListener: VideoPlaybackListener? = null
    3. //初始化
    4. private fun initMediaPlayer() {
    5. if (mMediaPlayer == null) {
    6. mMediaPlayer = MediaPlayer().apply {
    7. setAudioStreamType(AudioManager.STREAM_MUSIC)
    8. setOnPreparedListener {
    9. mPlaybackInfoListener?.onStateChanged(PlayState.PREPARED)
    10. mPlaybackInfoListener?.onDurationChanged(duration)
    11. }
    12. }
    13. }
    14. }
    15. //后续使用
    16. override fun play(videoUrl: String) {
    17. mMediaPlayer?.let {
    18. if (it.isPlaying && videoUrl == currentPlayingUrl) {
    19. it.start()
    20. mPlaybackInfoListener?.onStateChanged(PlayState.PLAYING)
    21. updateVideoProgress()
    22. }
    23. }
    24. }

    案例:数据模型类(data class)

    1. data class UserModel {
    2. var name: String?,
    3. var nick: String?,
    4. var tags: List?,
    5. var address: Address?,
    6. }

    解决:优先考虑val,其次赋予默认值

    1. data class UserModel {
    2. val name: String,
    3. val nick: String?,
    4. val tags: List = emptyList(),
    5. val address: Address?,
    6. }

    通用可空改造技巧

    案例:可空对象的方法调用,直接使用?.代替!!

    1. // case1
    2. if (mStateBarView != null) {
    3. mStateBarView!!.setBackgroundColor(color)
    4. }
    5. // case2
    6. if (mLoadingDialog != null && mLoadingDialog!!.isLoadingShowing) {
    7. return
    8. }
    9. if (activity != null) {
    10. mLoadingDialog = LoadingView()
    11. mLoadingDialog!!.showLoadingView(activity!!, loadingStyle)
    12. }

    解决:这种问题一般是由于?.和!!.不了解导致

    1. mStateBarView?.setBackgroundColor(color)
    2. if (mLoadingDialog?.isLoadingShowing == true) return
    3. activity?.let {
    4. mLoadingDialog = LoadingView()
    5. mLoadingDialog?.showLoadingView(it, loadingStyle)
    6. }

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

    1. val str1 = entity.str1!!
    2. useStr1(str1)

    解决1: 使用let,如果entity.str1为空则不执行useStr1(str1)

    1. entity.str1?.let {
    2. useStr1(it)
    3. }

    解决2:使用?: 或orEmpty() 赋予默认值,无论entity.str1是否为空,都执行useStr1(str1)

    1. val str1 = entity.str1.orEmpty()
    2. // 或者
    3. val str1 = entity.str1 ?: "" useStr1(str1)

    小技巧:两种方式都可行时,使用?: 或orEmpty() 赋予默认值,可以避免多个let造成的块复杂度增加

    案例:可空Int或Boolean比较

    1. if (viewModel.selectedMedia.value!!.size >= maxCount) {
    2. ToastUtil.showToast("")
    3. } if (dataModel.isVip!!) {
    4. //
    5. } if (dataModel.likeCount!! >= 0) {
    6. //
    7. } if (dataModel.likeCount!! == 0) {
    8. //
    9. }

    解决

    1. // 通过orEmpty() API提供默认值
    2. if (viewModel.selectedMedia.value.orEmpty().size >= maxCount) {
    3. ToastUtil.showToast("")
    4. }
    5. // 显式指定true
    6. if (dataModel.isVip == true) {
    7. }
    8. // 看情况是否使用(会影响代码可读性),使用默认值-1来表达整体条件空默认值的false
    9. if (dataModel.likeCount ?: -1 >= 0) {
    10. }
    11. // ==是equals的重载,左边可空(疑问)
    12. if (dataModel.likeCount == 0) {
    13. }

    案例:从方法调用的内部和外部触发

    1. fun handleDataList(list: List<DataModel>) {
    2. handle(list)
    3. }
    4. if (!list1.isNullOrEmpty()) {
    5. handleDataList(list1!!)
    6. } if (!list2.isNullOrEmpty()) {
    7. handleDataList(list2!!)
    8. } if (!list3.isNullOrEmpty()) {
    9. handleDataList(list3!!)
    10. }

    解决1:从函数外部考虑

    1. fun handleDataList(list: List<DataModel>) {
    2. handle(list)
    3. }
    4. handleDataList(list1.orEmpty())
    5. handleDataList (list2.orEmpty())
    6. handleDataList (list3.orEmpty())

    解决2:从函数内部考虑

    1. fun handleDataList(list: List<DataModel>?) {
    2. if (list.isNullOrEmpty()) return
    3. handle(list)
    4. }
    5. handleDataList(list1)
    6. handleDataList(list2)
    7. handleDataList(list3)

    小技巧:一般带else条件的判空很难使用let改造,可以从内部方法中着手改造(或者赋予默认值)

    1. // 考虑从handleDataList内部改造或赋予默认值
    2. if (model != null) {
    3. handleDataList(model.list!!)
    4. handleBoolean(model.flag!!)
    5. } else {
    6. Toast.makeText("");
    7. }

    待优化的问题

    优雅的解决类成员变量初始化时的强制可空

    案例:

    1. private val saleViewModel by lazy { ViewModelProvider(parentFragment!!)[SaleViewModel::class.java] }
    2. private val appInfo = IAppInfoProvider::class.impl()!!

    目前如要解决此问题,只能让这类变量为val可空类型,在每次使用时判空。

  • 相关阅读:
    【英语:语法基础】B8.核心语法-并列复合句和主从复合句
    淘宝/天猫API:item_sku-获取sku详细信息
    Vue3 - 局部指令(详细教程)
    什么是代理IP池?如何判断IP池优劣?
    合宙esp32c3不带串口芯片的型号如何arduino和micropython
    数学建模:智能优化算法及其python实现
    Spring Boot 使用 Passay 库的自定义密码验证器
    数据结构_最短路径Floyd算法(C语言)
    arraybuffer 转json
    springboot自定义参数解析器
  • 原文地址:https://blog.csdn.net/qq_39620460/article/details/126779981