Kotlin 我是否发现了一个bug,它能够在需要的地方获取空值?
几个月前,我学会了用Kotlin编程,并用它编写了大量代码。 最近,当我在做一个个人项目时,我尝试了以下方法:Kotlin 我是否发现了一个bug,它能够在需要的地方获取空值?,kotlin,nullpointerexception,Kotlin,Nullpointerexception,几个月前,我学会了用Kotlin编程,并用它编写了大量代码。 最近,当我在做一个个人项目时,我尝试了以下方法: 1 | sealed class Base(derivedRef: Any) { 2 | 3 | init { 4 | println("$derivedRef shouldn't be null !") 5 | } 6 | 7 | object Derived: Base(Derived) 8 | } 9 | 10|
1 | sealed class Base(derivedRef: Any) {
2 |
3 | init {
4 | println("$derivedRef shouldn't be null !")
5 | }
6 |
7 | object Derived: Base(Derived)
8 | }
9 |
10| fun main() {
11| val neededToInitializeDerived = Base.Derived
12| }
在编译时,没有问题:一切似乎都正常工作,我的IDE(Idea Intellij)没有用红色突出显示代码的任何部分。但是,一旦编译,如果我尝试运行它,它会打印一个奇怪的结果:
null不应该是null
仔细考虑后,似乎输入为Any
,并且由于空安全性,也不是空的derivedRef
,实际上是空的。
我的理论是,当我将派生的
对象本身作为其自身超类构造函数的参数传递时(实际上是派生的
类的单例实例,一旦编译,通过派生的.instance
),Derived.INSTANCE
未实例化,因为类Derived
本身及其超类基未实例化。它还接受一个临时的null
值,通常在编译时隐藏,在运行时不再可用,但我用这个特定的代码片段成功地捕获了这个值
问题是我现在有一个null
值,而不是正常的Any
值,只要我调用一个需要非null Any值的方法,就会抛出NullPointerException
;在使用这个小故障做一些不可能的事情时,我有一些乐趣,但我认为这可能是危险的,因为它不会在运行时崩溃,让您在调用上述方法之前自由运行代码,并使您的程序抛出意外错误,编译器确保您“不用担心,一切都很好”。
在进行了一些测试之后,这个bug产生了进一步的后果:我现在能够将null
值放入MutableList
中:
val theForbiddenValue:Any//
val myList=mutableListOf()
myList.add(null)//正常,不会编译
myList.add(theForbiddenValue)//编译!如果我打印它,我将获得“[null]”!
还有做一些根本不应该发生的奇怪的事情,比如定义应该返回Any
但不返回的函数等等。我最后的想法——我保证——通过使Base
implementsList
,例如,Derived
也将实现它。因此,您可以将derivedRef
的类型从Any
更改为List
,还可以在需要List
的位置获得null
值。这也适用于每个接口和/或非最终类
所以我的问题是,这是我刚刚发现的一个真正的bug吗?或者很多开发者已经知道了吗?或者,尽管外观如此,这是正常的(在这种情况下,我会非常惊讶)?这是关于构造顺序的-尽管它比大多数构造要微妙得多 构造问题最常见的顺序是,一般来说,构造函数不应该引用类中可以被子类覆盖(或更改)的任何内容 在Java和Kotlin中,构造函数总是首先调用超类构造函数(如果不编写显式调用,编译器会为您插入一个)。在返回后,它会运行任何字段初始化器,然后运行构造函数的其余部分。因此当超类构造函数运行时,尚未运行任何子类初始值设定项或构造函数代码。。这意味着非原语字段在该点将为空-即使它们是不可为空的类型 (我猜这并不是编译时错误,因为在某些情况下这是完全正确的:例如,如果一个子类重写了一个超类方法,但没有引用子类构造函数中重写或设置的任何字段。请注意IDE显示了一个警告,例如“访问非最终属性”[…]在构造函数中”或“调用非最终函数[…]在构造函数中”。) 然而,由于密封类和对象子类的组合,在这种情况下发生的事情就不那么明显了。据我所知,事件的顺序是:
- 因为
引用了main()
,所以它将构造Derived
对象Derived
- 派生的构造函数所做的第一件事是调用超类构造函数,将引用传递给它自己。但是,由于对象尚未构造,它的引用似乎为空。← 这当然是bug的原因,理想情况下会给出一个编译时错误,或者至少是一个警告
- 超类构造函数运行,并打印出您看到的消息
- 子类构造函数的其余部分运行(并且什么也不做)。至此,
给出了一个有效的引用,但到那时已经太晚了Derived
- 运行
方法的其余部分,设置其局部变量main()
这里有一个更能说明顺序的变体(我将对象移到密封类之外,尽管这没有实际意义。我还将
derivedRef
作为一个属性,这样我们可以在后面看到它)
这会打印出如下内容:
Base.init: Derived = null, derivedRef = null
Derived.init: Derived = Derived@87aac27, derivedRef = null
main: Derived = Derived@87aac27, derivedRef = null
您可以看到,一旦超类构造函数完成,甚至在它自己的构造函数完成之前,Derived
引用就有效;但是对于已经设置为null的属性来说,这当然太晚了
顺便说一句,如果将对象生成一个普通类,则不会发生这种情况:当它调用超类构造函数时,它必须传递
this
,但编译器会抱怨“this”未在此上下文中定义”
或者,如果密封类被构造成一个普通的开放类,那么它编译OK,但给出一个N
sealed class Base(val derivedRef: Any) {
init {
println("Base.init: Derived = $Derived, derivedRef = $derivedRef")
}
}
object Derived: Base(Derived) {
init {
println("Derived.init: Derived = $Derived, derivedRef = $derivedRef")
}
}
fun main() {
println("main: Derived = $Derived, derivedRef = ${Derived.derivedRef}")
}
Base.init: Derived = null, derivedRef = null
Derived.init: Derived = Derived@87aac27, derivedRef = null
main: Derived = Derived@87aac27, derivedRef = null