kotlinelvis-operator

How to run a piece of code before continue/break/return statement in Kotlin elvis operator?


Imagine a function like this:

fun foo(bar: Bar) {
    val bar2: Bar = bar.nullableThing ?: return
    // do something with bar2
}

This works perfectly until you want to run something (like showing a message) before that "return" statement. I think it would look like this:

fun foo(bar: Bar) {
    val bar2: Bar = bar.nullableThing ?: run { 
        println("Error!")
        return
    }
    // do something with bar2
}

but that does not work ('return' is not allowed here).

Edit: My issue was actually with the continue statement, I did not know this would work with return. Here is a new piece of code that is more similar to the actual code:

data class Item(val str: String?)

val items = listOf<Item>(Item("tag:a"), Item("tag:b"), Item(null), Item("tag:c"))

val taggedItems = mapOf<String, String>("a" to "ItemA", "b" to "ItemB")

fun main() {
    for (item in items) {
        val stringItem = item.str?.let {
            if (it.startsWith("tag:")) taggedItems[it.substringAfter("tag:")]
            else it
        } ?: run {
            println("Error: tag ${item.str} is invalid")
            continue
        }
        compute(stringItem)
    }
}

fun compute(itemStr: String) = println(itemStr)

https://pl.kotl.in/SSU_z-Ctu The error is 'break' or 'continue' jumps across a function or a class boundary.

Given this, I could create a function like this:

fun printError(item: Item): String? {
    println("Error: tag ${item.str} is invalid")
    return null
}

And then

... ?: printError(item) ?: continue

butt that would be awful


Solution

  • You could just revert to traditional coding style:

    fun main() {
        for (item in items) {
            val stringItem = item.str?.let {
                if (it.startsWith("tag:")) taggedItems[it.substringAfter("tag:")]
                else it
            }
            if (stringItem == null) {
                println("Error: tag ${item.str} is invalid")
                continue
            }
            // From here on, stringItem is smart-cast as non-nullable
            compute(stringItem)
        }
    }
    

    Though I would personally prefer avoiding anything that behaves similarly to a goto, such as continue:

    fun main() {
        for (item in items) {
            val stringItem = item.str?.let {
                if (it.startsWith("tag:")) taggedItems[it.substringAfter("tag:")]
                else it
            }
            if (stringItem != null) {
                compute(stringItem)
            } else {
                println("Error: tag ${item.str} is invalid")
            }
        }
    }
    

    But if you really, really like to use scope functions as much as possible:

    fun main() {
        for (item in items) {
            item.str?.let {
                if (it.startsWith("tag:")) taggedItems[it.substringAfter("tag:")]
                else it
            }.also {
                compute(it)
            } ?: println("Error: tag ${item.str} is invalid")
        }
    }