简体   繁体   中英

Null check with ?. vs ?: fails in kotlin

I have following statement in my code:

safeOrderResult.accomplished?.let{ safeAccomplished->
  //Do something with safeAccomplished when accomplished <> null

  Log.i(TAG,"bind: safeOrderResult.accomplishedId.let?{}") 
}?:let{

  //Do something when accomplished == null

  Log.i(TAG,"bind: safeOrderResult.accomplishedId?:let{} *null*" )
}

Here my code does something strange:

On a Samsung TAB A (i think not significant) it works as expected.

On a Samsung S9 it calls both let sections.

Snippet from Logcat Samsung S9 (android 10)

2021-05-06 14:11:35.427 9069-9069/no.norva24.mslam I/ViewHolder: bind: safeOrderResult.accomplishedId = 408
2021-05-06 14:11:35.427 9069-9069/no.norva24.mslam I/ViewHolder: bind: safeOrderResult.accomplishedId.let?.{}
2021-05-06 14:11:35.427 9069-9069/no.norva24.mslam I/ViewHolder: bind: handleDate = null <- inside above let: ok 
2021-05-06 14:11:35.427 9069-9069/no.norva24.mslam I/ViewHolder: bind: safeOrderResult.accomplishedId?:let{} *null*
2021-05-06 14:11:35.427 9069-9069/no.norva24.mslam I/ViewHolder: bind: flagged = false or null

TabA: android 10

2021-05-06 14:21:16.676 2468-2468/no.norva24.mslam I/ViewHolder: bind: safeOrderResult.accomplishedId = 427
2021-05-06 14:21:16.676 2468-2468/no.norva24.mslam I/ViewHolder: bind: safeOrderResult.accomplishedId.let?.{}
2021-05-06 14:21:16.678 2468-2468/no.norva24.mslam I/ViewHolder: bind: handleDate = null <-- inside above let
2021-05-06 14:21:16.685 2468-2468/no.norva24.mslam I/ViewHolder: bind: flagged = false or null

The key point is, how can a value both be null and contain a value?, or can kotlin "change" to null and kick in in the second "null" let, if value has changed in the first first let (which I didn't do)

I am Using kotlin 1.5.0

EDIT 2021.05.06 18:55 GMT+2

I am not sure, but I might have learned something here today: ;)

safeOrderResult.accomplished?.let{ safeAccomplished->

  //Do something with safeAccomplished when accomplished <> null

  /*Here I have preserved my value in safeAccomplished
    And actually returning a value below (a Unit()) from Log.i ?*/
  Log.i(TAG,"bind: safeOrderResult.accomplishedId.let?{}") 
}?:let{
   
  //Do something when accomplished == null

  /* But why did the code kick in here ?
     After it was inside the let above ? I thought the '?:let' was 
     continuing if the '?.let' didn't kick in. 
     */
  Log.i(TAG,"bind: safeOrderResult.accomplishedId?:let{} *null*" )
}

/*
Below is the actual code which had the trouble (the code isn't finished therefore the "preserved" `let` values isn't used)
*/

                        safeOrderResult.accomplishedId?.let {
                            listItemOrderListLinearLayoutCompatStatus.apply {
                                visibility = View.VISIBLE
                                listItemOrderListMaterialTextViewOrderStatus.text =
                                    context.resources.getStringArray(
                                        R.array.basic_register_accomplish_status_names)[1]
                                listItemOrderListMaterialTextViewDate.text =
                                    dateStringSplitSpace(safeOrderResult.registeredDate)
                                Log.i(TAG, "bind: handleDate = ${safeOrderResult.handleDate}")
                                listItemOrderListMaterialTextViewReason.text =
                                    if(safeOrderResult.handleDate.isNullOrEmpty())
                                        "Still possible to update"
                                    else
                                        "Assignment locked on ${dateStringSplitSpace(safeOrderResult.handleDate)}"
                                setBackgroundColor(
                                    ContextCompat.getColor(
                                        itemView.context,
                                        if(safeOrderResult.handleDate.isNullOrEmpty())
                                            R.color.list_item_register_field_accomplished_background
                                        else
                                            R.color.list_item_register_field_accomplished_locked_background
                                    )
                                )

                            }
                            listItemOrderListLinearLayoutCompatStatusMore?.apply {
                                setBackgroundColor(
                                    ContextCompat.getColor(
                                        itemView.context,
                                        if(safeOrderResult.handleDate.isNullOrEmpty())
                                            R.color.list_item_register_field_accomplished_background
                                        else
                                            R.color.list_item_register_field_accomplished_locked_background
                                    )
                                )
                            }
                        }?:let {
                            safeOrderResult.passedId?.let { safePassedId->
                                listItemOrderListLinearLayoutCompatStatus.apply {
                                    visibility = View.VISIBLE
                                    listItemOrderListMaterialTextViewOrderStatus.text =
                                        context.resources.getStringArray(
                                            R.array.basic_register_accomplish_status_names
                                        )[2]
                                    listItemOrderListMaterialTextViewDate.text =
                                        dateStringSplitSpace(safeOrderResult.registeredDate)
                                    listItemOrderListMaterialTextViewReason.text =
                                        safeOrderResult.passedReason
                                    setBackgroundColor(
                                        ContextCompat.getColor(
                                            itemView.context,
                                            R.color.list_item_register_field_passed_background,
                                        )
                                    )
                                }
                            }?:let {
                                listItemOrderListLinearLayoutCompatStatus.apply {
                                    visibility = View.GONE
                                }
                            }
                        }

** ADDENDUM 2020.05.06 19:30 GMT+2 **

In playground I got trouble with this:

/**
 * You can edit, run, and share this code. 
 * play.kotlinlang.org 
 */

class A {
    fun f() {
        let { println(it) }
    }
}

data class DataClass(
    var value1:String?=null,
    var value2:String?=null
)

fun main() {
    A().f()
    
    var myData = DataClass()
    
    myData.value1 = "1"
    
    
    myData.value1?.let{ safeValue1->
        println("value1 = "+safeValue1)
    }?:let{
        println("value1==null !!")
    }
    
    myData.value2?.let{ safeValue2->
        println("value2 = "+safeValue2)
    }?:let{
        println("value2==null !!")
    }   
    
    
}

Where it kicked on the ?:let 's above. This was ok in kotin v.1.5.0 at least...

ADDENDUM 2: 2020.05.06 19:40 GMT+2

在此处输入图像描述

So... dataClass.value?:let{ } isn't allowed? in a 'standard' kotlin scenario to check for null existence?, but still 'valid' in AS2020.3.1.15 w/kotlin 1.5.0 ...

ADDENDUM 3: 2020.05.06 19:55 GMT+2

When using another approach (omitting let keyword in ?:let{ I got this answer to the based on the playground code above:

在此处输入图像描述

Here I expected also the value2 to show up with value2==null !! but it didn`t...

Here's the playground code now:

/**
 * You can edit, run, and share this code. 
 * play.kotlinlang.org 
 */

class A {
    fun f() {
        let { println(it) }
    }
}

data class DataClass(
    var value1:String?=null,
    var value2:String?=null
)

fun main() {
    A().f()
    
    var myData = DataClass()
    
    myData.value1 = "1"
    
    /*
    myData.value1?.let{ safeValue1->
        println("value1 = "+safeValue1)
    }?:let{
        println("value1==null !!")
    }
    
    myData.value2?.let{ safeValue2->
        println("value2 = "+safeValue2)
    }?:let{
        println("value2==null !!")
    }   
    */
    
    myData.value1?.let{ safeValue1->
        println("value1 = "+safeValue1)
    }
    
    myData.value1?:{
        println("value1==null !!")
    }
    
    myData.value2?.let{ safeValue2->
        println("value2 = "+safeValue2)
    }
    
    myData.value2?:{
        println("value2==null !!")
    }
    
}

...still a little confused...

The let function can indeed change your target to null. It changes the target to whatever it returns. A lambda implicitly returns the result of its last expression. Your code above has a Log.i() call as its last expression, so it returns Unit, so the second let function should never run if the first one does. Is it possible you've snipped off some code at the end of your first let lambda that could possibly return a null value?

A quick fix for the above problem would be to swap let for also , because also always returns its receiver.

I think most experienced Kotlin users will advise you not to chain scope function calls like this because it makes the code hard to follow and it is easy to introduce subtle bugs. You can write a more robust version like this:

val accomplished = safeOrderResult.accomplished
if (accomplished != null) {
    //Do something with smart-cast non-nullable accomplished
} else {
    //Do something when accomplished == null
}

At a guess, the first one is returning null at the end, which means the value produced by that whole expression is null , so the stuff after the ?: is triggered (since that's an "if the left side evaluates to null" condition).

Why that would only happen on some Samsung models - who knows, they have a history of messing with things in the Android library. I'd check exactly what's going on in the block and what it might evaluate to. You might need to return Unit at the end, or use a function like apply that returns the receiver instead of the result of the lambda.

This is why the if/else is a better fit - you have a condition at the start, and you decide whether to do one thing or another, exclusively. let produces a value, and it's often used to propagate a value down a chain, and return a result. ?: is a final default value, for if that result turns out to be null.

It's absolutely possible to run the let block and the code after the ?: , and sometimes that a thing you want to do. As a construction it's often used for returning a default value. So if/else is a little more explicit about what you're doing and how it's meant to work, and it helps avoid surprise bugs like this one!

You can do an if-null-else with ?.let but it's not very readable in my opinion

var s: String? = "Str"
s?.let { println("A ok") } ?: run { println("A null") }
s = null
s?.let { println("B ok") } ?: run { println("B null") }
A ok
B null

It is also possible to introduce subtle bugs like this:

var s: String? = "Str"
s?.let { println("A ok"); null } ?: run { println("A null") }
A ok
A null

This is why you should use an if-else if you both need the non-null and null case. ( ?. is intended for the case where only the non-null case makes sense):

if (s == null) println("A null") else println("A ok")
if (s == null) { 
    println("A null") 
} else { 
    println("A ok") 
}

If you don't want to bind accomplished to a variable as in @Tenfour04's answer, I'd write it as

safeOrderResult.accomplished.let {
    if (it != null) {
        // safeOrderResult.accomplished was not null, use it
    } else {
        // safeOrderResult.accomplished was null
    }
}

or

safeOrderResult.accomplished.let { accomplished ->
    if (accomplished != null) {
        // safeOrderResult.accomplished was not null, use accomplished
    } else {
        // safeOrderResult.accomplished was null
    }
}

Note .let and not ?.let . But pick on readability/convenience. I definitely wouldn't use

value?.let{ safeValue-> /*dowork*/}; value?:let{/*do null work*/}

as you suggest in another comment.

Thanx for many good answers above, and you all are right...

I landed on following solution for my problem, but still not quite happy though:

I use .apply to remove some value. overhead,

safeOrderResult.apply{

   if(accomplished!=null){
        //Do something with accomplished since accomplished <> null

        Log.i(TAG,"bind: accomplished != null") 
   }else{

        //Do something when accomplished == null

        Log.i(TAG,"bind: accomplished == null" )
   }
}

I mark accepted for @Alexey Romanov suggestion which is quite reasonable.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM