8

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.

Grzegorz Piwowarek
  • 13,172
  • 8
  • 62
  • 93
  • `foo.run { if (this == null) doThis() else doThat(this) }` is totally legit. The usability is debatable. – Eugen Pechanec Aug 02 '17 at 20:52
  • 1
    simply because `'run` is an extension method on a generic type T and the block is also an extension method on T. The variable `foo` value isn't what's important, it the inferred type. `foo` is passed to the block as `this`, and as long as `this` is not use, a `null` valued `foo` is safe. – Les Aug 03 '17 at 00:51

3 Answers3

11

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?
hotkey
  • 140,743
  • 39
  • 371
  • 326
4

To add to what @holi-java said, there is nothing unsafe about your code at all. println("foo") is perfectly valid whether foo is null or not. If you tried something like

foo.run { subString(1) }

it would be unsafe, and you will find it won't even compile without some sort of null check:

foo.run { this?.subString(1) }
// or
foo?.run { subString(1) }
Ruckus T-Boom
  • 4,566
  • 1
  • 28
  • 42
2

This is because the top-level function run accept anything Any & Any?. so an extension function with Null Receiver doesn't checked by Kotlin in runtime.

//                 v--- accept anything
public inline fun <T, R> T.run(block: T.() -> R): R = block()

Indeed, the inline function run is generated by Kotlin without any assertions if the receiver can be nullable, so it is more like a noinline function generated to Java code as below:

public static Object run(Object receiver, Function1<Object, Object> block){
   //v--- the parameters checking is taken away if the reciever can be nullable
  //Intrinsics.checkParameterIsNotNull(receiver, "receiver");

  Intrinsics.checkParameterIsNotNull(block, "block");
  // ^--- checking the `block` parameter since it can't be null 
}

IF you want to call it in a safety way, you can use safe-call operator ?. instead, for example:

val foo: String? = null

// v--- short-circuited if the foo is null
foo?.run { println("foo") }
holi-java
  • 29,655
  • 7
  • 72
  • 83