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

╄→尐↘猪︶ㄣ 提交于 2019-12-01 18:14:38

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") }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!