Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do unsafe .run() call works fine on a null value in Kotlin?

I have the following code fragment:

val foo: String? = null
foo.run { println("foo") }

I have here a nullable variable foo that is actually set to null followed by a nonsafe .run() call.

When I run the code snippet, I get foo printed out despite the fact that the run method is called on a null. Why is that? Why no NullPointerException? Why does compiler allow a nonsafe call on an optional value?

If I pass println(foo), I get a nice juicy null in the console so I think it's safe to assume that foo is actually null.

like image 737
Grzegorz Piwowarek Avatar asked Aug 02 '17 20:08

Grzegorz Piwowarek


1 Answers

I believe, there are two things that both might be of some surprise: the language semantics that allow such a call, and what happens at runtime when this code executes.

From the language side, Kotlin allows nullable receiver, but only for extensions. To write an extension function that accepts a nullable receiver, one should either write the nullable type explicitly, or use a nullable upper bound for a type parameter (actually, when you specify no upper bound, the default one is nullable Any?):

fun List<*>?.isEmptyOrNull() = this == null || size == 0 // explicit nullable type

fun <T : CharSequence?> T.nullWhenEmpty() = if ("$this" == "") null else this // nullable T

fun <T> T.identity() = this // default upper bound Any? is nullable

This feature is used in kotlin-stdlib in several places: see CharSequence?.isNullOrEmpty(), CharSequence?.isNullOrBlank(), ?.orEmpty() for containers and String?.orEmpty(), and even Any?.toString(). Some functions like T.let, T.run that you asked about and some others just don't provide an upper bound for the type parameter, and that defaults to nullable Any?. And T.use provides a nullable upper bound Closeable?.

Under the hood, that is, from the runtime perspective, the extension calls are not compiled into the JVM member call instructions INVOKEVIRTUAL, INVOKEINTERFACE or INVOKESPECIAL (the JVM checks the first argument of such calls, the implicit this, for being null and throws an NPE if it is, and this is how Java & Kotlin member functions are called). Instead, the Kotlin extension functions are compiled down to static methods, and the receiver is just passed as the first argument. Such a method is called with the INVOKESTATIC instruction that does not check the arguments for being null.

Note that when a receiver of an extension can be nullable, Kotlin does not allow you to use it where a not-null value is required without checking it for null first:

fun Int?.foo() = this + 1 // error, + is not defined for nullable Int?
like image 161
hotkey Avatar answered Sep 21 '22 07:09

hotkey