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
.
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?
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) }
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") }