Search code examples
androidkotlinnullablestatic-analysis

How can I tell kotlin that a function doesn't return null if the parameter is not null?


I want to write a convenience extension to extract values from a Map while parsing them at the same time. If the parsing fails, the function should return a default value. This all works fine, but I want to tell the Kotlin compiler that when the default value is not null, the result won't be null either. I could to this in Java through the @Contract annotation, but it seems to not work in Kotlin. Can this be done? Do contracts not work for extension functions? Here is the kotlin attempt:

import org.jetbrains.annotations.Contract

private const val TAG = "ParseExtensions"

@Contract("_, !null -> !null")
fun Map<String, String>.optLong(key: String, default: Long?): Long? {
    val value = get(key)
    value ?: return default

    return try {
        java.lang.Long.valueOf(value)
    } catch (e: NumberFormatException) {
        Log.e(TAG, e)
        Log.d(TAG, "Couldn't convert $value to long for key $key")

        default
    }
}

fun test() {
    val a = HashMap<String, String>()

    val something: Long = a.optLong("somekey", 1)
}

In the above code, the IDE will highlight an error in the assignment to something despite optLong being called with a non null default value of 1. For comparison, here is similar code which tests nullability through annotations and contracts in Java:

public class StackoverflowQuestion
{
    @Contract("_, !null -> !null")
    static @Nullable Long getLong(@NonNull String key, @Nullable Long def)
    {
        // Just for testing, no real code here.
        return 0L;
    }

    static void testNull(@NonNull Long value) {
    }

    static void test()
    {
        final Long something = getLong("somekey", 1L);
        testNull(something);
    }
}

The above code doesn't show any error. Only when the @Contract annotation is removed will the IDE warn about the call to testNull() with a potentially null value.


Solution

  • You can do this by making the function generic.

    fun <T: Long?> Map<String, String>.optLong(key: String, default: T): T 
    {
        // do something.
        return default
    }
    

    Which can be used like this:

    fun main(args: Array<String>) {
        val nullable: Long? = 0L
        val notNullable: Long = 0L
    
        someMap.optLong(nullable) // Returns type `Long?`
        someMap.optLong(notNullable) // Returns type `Long`
    }
    

    This works because Long? is a supertype of Long. The type will normally be inferred in order to return a nullable or non-nullable type based on the parameters.

    This will "tell the Kotlin compiler that when the default value is not null, the result won't be null either."