为什么不安全的.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.let
, T.run
这样的函数,你问了一些其他的只是没有提供一个types参数的上限,并且默认为可空Any?
。 T.use
提供了一个可空的上限可Closeable?
。
在这种情况下,也就是从运行时的角度来看,扩展调用并没有被编译到JVM成员调用指令INVOKEVIRTUAL
, INVOKEINTERFACE
或INVOKESPECIAL
(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
接受Any
& Any?
。 因此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
如果您想以安全的方式呼叫,您可以使用安全呼叫操作员?.
相反,例如:
val foo: String? = null // v--- short-circuited if the foo is null foo?.run { println("foo") }