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) {
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可空类型,在每次使用时判空。