为什么不安全的.run()调用在Kotlin的空值上工作正常?

我有以下代码片段:

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

我在这里有一个可空的variablesfoo ,它实际上被设置为null然后是非安全的.run()调用。

当我运行代码片段时,尽管在null上调用了run方法,我仍然可以打印出来。 这是为什么? 为什么没有NullPointerException ? 为什么编译器允许对一个可选值进行非安全调用?

如果我传递println(foo) ,我在控制台中得到了一个很好的null ,所以我认为foo实际上是null是安全的。

我认为,有两件事情可能会让人感到意外:语言语义允许这样的调用,以及执行代码时在运行时会发生什么。

从语言方面来说,Kotlin允许可以为空的接收方,但只能用于扩展 。 要写一个接受可空接收者的扩展函数,你应该显式地写可空types,或者为一个types参数使用一个可为空的上限(实际上,当你指定没有上界时,默认值是空的Any? ):

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

 fun  T.nullWhenEmpty() = if ("$this" == "") null else this // nullable T 

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

这个特性被用在kotlin-stdlib中的几个地方:查看CharSequence?.isNullOrEmpty()CharSequence?.isNullOrBlank()?.orEmpty()用于容器和String?.orEmpty() ,甚至是Any?.toString() 。 一些像T.letT.run这样的函数,你问了一些其他的只是没有提供一个types参数的上限,并且默认为可空Any?T.use提供了一个可空的上限可Closeable?

在这种情况下,也就是从运行时的角度来看,扩展调用并没有被编译到JVM成员调用指令INVOKEVIRTUALINVOKEINTERFACEINVOKESPECIAL (JVM检查这些调用的第一个参数,隐式的,为null并抛出NPE,如果是的话,这就是Java和Kotlin成员函数的调用方式)。 相反,Kotlin扩展函数被编译成静态方法,而接收者只是作为第一个parameter passing。 用INVOKESTATIC指令调用这种方法,不检查参数为空。

请注意,当一个扩展的接收者可以为空时,Kotlin不允许你在需要非null的地方使用它,而不要首先检查它是否为null:

 fun Int?.foo() = this + 1 // error, + is not defined for nullable Int? 

要添加@ holi-java所说的,根本就不存在对代码不安全的问题。 println("foo")是完全有效的,无论foo是否为空。 如果你尝试类似的东西

 foo.run { subString(1) } 

这将是不安全的,你会发现它甚至不会没有某种空检查的编译:

 foo.run { this?.subString(1) } // or foo?.run { subString(1) } 

这是因为顶级函数run接受AnyAny? 。 因此Kotlin在运行时不会检查带有Null Receiver的扩展函数。

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

实际上 ,如果receiver可以为 ,内联函数run是由Kotlin生成的,没有任何断言,所以它更像是Java代码生成的noinline函数,如下所示:

 public static Object run(Object receiver, Function1 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 } 

如果您想以安全的方式呼叫,您可以使用安全呼叫操作员?. 相反,例如:

 val foo: String? = null // v--- short-circuited if the foo is null foo?.run { println("foo") }