Idiomatic way of handling nullable or empty List in Kotlin

大憨熊 提交于 2019-12-03 04:50:32

问题


Say I have a variable activities of type List<Any>?. If the list is not null and not empty, I want to do something, otherwise I want to do something else. I came up with following solution:

when {
    activities != null && !activities.empty -> doSomething
    else -> doSomethingElse
}

Is there a more idiomatic way to do this in Kotlin?


回答1:


For some simple actions you can use the safe call operator, assuming the action also respects not operating on an empty list (to handle your case of both null and empty:

myList?.forEach { ...only iterates if not null and not empty }

For other actions. you can write an extension function -- two variations depending on if you want to receive the list as this or as a parameter:

inline fun <E: Any, T: Collection<E>> T?.withNotNullNorEmpty(func: T.() -> Unit): Unit {
    if (this != null && this.isNotEmpty()) {
        with (this) { func() }
    }
}

inline fun  <E: Any, T: Collection<E>> T?.whenNotNullNorEmpty(func: (T) -> Unit): Unit {
    if (this != null && this.isNotEmpty()) {
        func(this)
    }
}

Which you can use as:

fun foo() {  
    val something: List<String>? = makeListOrNot()
    something.withNotNullNorEmpty { 
        // do anything I want, list is `this`
    }

    something.whenNotNullNorEmpty { myList ->
        // do anything I want, list is `myList`
    }
}

You can also do inverse function:

inline fun <E: Any, T: Collection<E>> T?.withNullOrEmpty(func: () -> Unit): Unit {
    if (this == null || this.isEmpty()) {
        func()
    }
}

I would avoid chaining these because then you are replacing an if or when statement with something more wordy. And you are getting more into the realm that the alternatives I mention below provide, which is full branching for success/failure situations.

Note: these extensions were generalized to all descendants of Collections holding non null values. And work for more than just Lists.

Alternatives:

The Result library for Kotlin gives a nice way to handle your case of "do this, or that" based on response values. For Promises, you can find the same thing in the Kovenant library.

Both of these libraries give you manner for returning alternative results from a single function, and also for branching the code based on the results. They do require that you are controlling the provider of the "answer" that is acted upon.

These are good Kotlin alternatives to Optional and Maybe.

Exploring the extension Functions Further (and maybe too much)

This section is just to show that when you hit an issue like the question raised here, you can easily find many answers in Kotlin to make coding the way you want it to be. If the world isn't likeable, change the world. It isn't intended as a good or bad answer, but rather additional information.

If you like the extension functions and want to consider chaining them in an expression, I would probably change them as follows...

The withXyz flavours to return this and the whenXyz should return a new type allowing the whole collection to become some new one (maybe even unrelated to the original). Resulting in code like the following:

val BAD_PREFIX = "abc"
fun example(someList: List<String>?) {
    someList?.filterNot { it.startsWith(BAD_PREFIX) }
            ?.sorted()
            .withNotNullNorEmpty {
                // do something with `this` list and return itself automatically
            }
            .whenNotNullNorEmpty { list ->
                // do something to replace `list` with something new
                listOf("x","y","z")
            }
            .whenNullOrEmpty {
                // other code returning something new to replace the null or empty list
                setOf("was","null","but","not","now")
            }
}

Note: full code for this version is at the end of the post (1)

But you could also go a completely new direction with a custom "this otherwise that" mechanism:

fun foo(someList: List<String>?) {
    someList.whenNullOrEmpty {
        // other code
    }
    .otherwise { list ->
        // do something with `list`
    }
}

There are no limits, be creative and learn the power of extensions, try new ideas, and as you can see there are many variations to how people want to code these type of situations. The stdlib cannot support 8 variations of these type of methods without being confusing. But each development group can have extensions that match their coding style.

Note: full code for this version is at the end of the post (2)

Sample code 1: Here is the full code for the "chained" version:

inline fun <E: Any, T: Collection<E>> T?.withNotNullNorEmpty(func: T.() -> Unit): T? {
    if (this != null && this.isNotEmpty()) {
        with (this) { func() }
    }
    return this
}

inline fun  <E: Any, T: Collection<E>, R: Any> T?.whenNotNullNorEmpty(func: (T) -> R?): R? {
    if (this != null && this.isNotEmpty()) {
        return func(this)
    }
    return null
}

inline fun <E: Any, T: Collection<E>> T?.withNullOrEmpty(func: () -> Unit): T? {
    if (this == null || this.isEmpty()) {
        func()
    }
    return this
}

inline fun <E: Any, T: Collection<E>, R: Any> T?.whenNullOrEmpty(func: () -> R?): R?  {
    if (this == null || this.isEmpty()) {
        return func()
    }
    return null
}

Sample Code 2: Here is the full code for a "this otherwise that" library (with unit test):

inline fun <E : Any, T : Collection<E>> T?.withNotNullNorEmpty(func: T.() -> Unit): Otherwise {
    return if (this != null && this.isNotEmpty()) {
        with (this) { func() }
        OtherwiseIgnore
    } else {
        OtherwiseInvoke
    }
}

inline fun  <E : Any, T : Collection<E>> T?.whenNotNullNorEmpty(func: (T) -> Unit): Otherwise {
    return if (this != null && this.isNotEmpty()) {
        func(this)
        OtherwiseIgnore
    } else {
        OtherwiseInvoke
    }
}

inline fun <E : Any, T : Collection<E>> T?.withNullOrEmpty(func: () -> Unit): OtherwiseWithValue<T> {
    return if (this == null || this.isEmpty()) {
        func()
        OtherwiseWithValueIgnore<T>()
    } else {
        OtherwiseWithValueInvoke(this)
    }
}

inline fun <E : Any, T : Collection<E>> T?.whenNullOrEmpty(func: () -> Unit): OtherwiseWhenValue<T> {
    return if (this == null || this.isEmpty()) {
        func()
        OtherwiseWhenValueIgnore<T>()
    } else {
        OtherwiseWhenValueInvoke(this)
    }
}

interface Otherwise {
    fun otherwise(func: () -> Unit): Unit
}

object OtherwiseInvoke : Otherwise {
    override fun otherwise(func: () -> Unit): Unit {
        func()
    }
}

object OtherwiseIgnore : Otherwise {
    override fun otherwise(func: () -> Unit): Unit {
    }
}

interface OtherwiseWithValue<T> {
    fun otherwise(func: T.() -> Unit): Unit
}

class OtherwiseWithValueInvoke<T>(val value: T) : OtherwiseWithValue<T> {
    override fun otherwise(func: T.() -> Unit): Unit {
        with (value) { func() }
    }
}

class OtherwiseWithValueIgnore<T> : OtherwiseWithValue<T> {
    override fun otherwise(func: T.() -> Unit): Unit {
    }
}

interface OtherwiseWhenValue<T> {
    fun otherwise(func: (T) -> Unit): Unit
}

class OtherwiseWhenValueInvoke<T>(val value: T) : OtherwiseWhenValue<T> {
    override fun otherwise(func: (T) -> Unit): Unit {
        func(value)
    }
}

class OtherwiseWhenValueIgnore<T> : OtherwiseWhenValue<T> {
    override fun otherwise(func: (T) -> Unit): Unit {
    }
}


class TestBrancher {
    @Test fun testOne() {
        // when NOT null or empty

        emptyList<String>().whenNotNullNorEmpty { list ->
            fail("should not branch here")
        }.otherwise {
            // sucess
        }

        nullList<String>().whenNotNullNorEmpty { list ->
            fail("should not branch here")
        }.otherwise {
            // sucess
        }

        listOf("a", "b").whenNotNullNorEmpty { list ->
            assertEquals(listOf("a", "b"), list)
        }.otherwise {
            fail("should not branch here")
        }

        // when YES null or empty

        emptyList<String>().whenNullOrEmpty {
            // sucess
        }.otherwise { list ->
            fail("should not branch here")
        }

        nullList<String>().whenNullOrEmpty {
            // success
        }.otherwise {
            fail("should not branch here")
        }

        listOf("a", "b").whenNullOrEmpty {
            fail("should not branch here")
        }.otherwise { list ->
            assertEquals(listOf("a", "b"), list)
        }

        // with NOT null or empty

        emptyList<String>().withNotNullNorEmpty {
            fail("should not branch here")
        }.otherwise {
            // sucess
        }

        nullList<String>().withNotNullNorEmpty {
            fail("should not branch here")
        }.otherwise {
            // sucess
        }

        listOf("a", "b").withNotNullNorEmpty {
            assertEquals(listOf("a", "b"), this)
        }.otherwise {
            fail("should not branch here")
        }

        // with YES null or empty

        emptyList<String>().withNullOrEmpty {
            // sucess
        }.otherwise {
            fail("should not branch here")
        }

        nullList<String>().withNullOrEmpty {
            // success
        }.otherwise {
            fail("should not branch here")
        }

        listOf("a", "b").withNullOrEmpty {
            fail("should not branch here")
        }.otherwise {
            assertEquals(listOf("a", "b"), this)
        }


    }

    fun <T : Any> nullList(): List<T>? = null
}



回答2:


UPDATE:

kotlin 1.3 provide isNullOrEmpty now!

https://twitter.com/kotlin/status/1050426794682306562


try this! very clear.

var array: List<String>? = null
if (array.orEmpty().isEmpty()) {
    // empty
} else {
    // not empty
}



回答3:


In addition to the other answers, you can also use the safe-call operator in combination with the extension method isNotEmpty(). Because of the safe call, the return value is actually Boolean? which can either be true, false or null. To use the expression in an if or when clause, you'll need to explictly check if it's true:

when {
    activities?.isNotEmpty() == true -> doSomething
    else -> doSomethingElse
}

Alternative syntax using the elvis operator:

when {
    activities?.isNotEmpty() ?: false -> doSomething
    else -> doSomethingElse
}



回答4:


More simplest way would be,

if(activities?.isNotEmpty() == true) doSomething() else doSomethingElse()



回答5:


Consider using ?.forEach if appropriate

activities?.forEach {
  doSmth(it)
}

If you want exactly the behavior you described I think your variant reads better then anything else more concise I can think of. (Yet simple if should suffice)




回答6:


In my case prices is a optional.I handle the case in the following way with orEmpty() which returns the given array or an empty array if the given array is null.

val safeArray  = poi.prices.orEmpty()
if (!safeArray.isEmpty()) {
   ...
}



回答7:


The actual method to use in Kotlin 1.3 is isNullOrEmpty like what was mentioned in this answer: https://stackoverflow.com/a/48056456/2735286

Here is an example of its usage:

fun main(args: Array<String>) {
    var array: MutableList<String>? = null
    println(array.isNullOrEmpty()) // true
    array = mutableListOf()
    println(array.isNullOrEmpty()) // true
    array = mutableListOf("a")
    println(array.isNullOrEmpty()) // false
}

This example prints out:

true
true
false



回答8:


Kotlin 1.3 has extension isNullOrEmpty. The brief answer is:

if (activities.isNullOrEmpty) doSomething
else doSomethingElse

Extension is defined as:

fun <T> Collection<T>?.isNullOrEmpty(): Boolean

Similar extension exists for String and Array.




回答9:


Firstly I wanted to advise to make extension function in addition to @mlatu's answer, which handles else condition

 
public inline fun  Map.forEachElse(operation: (Map.Entry) -> Unit, elseBlock: () -> Unit): Unit {
        if (!empty)
            for (element in this) operation(element)
        else
            elseBlock()
    }

But the usage is no so beautiful.

Actually you are looking for a Maybe monad



来源:https://stackoverflow.com/questions/26341225/idiomatic-way-of-handling-nullable-or-empty-list-in-kotlin

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!