Kotlin 我是否发现了一个bug,它能够在需要的地方获取空值?

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|

几个月前,我学会了用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|  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
implements
List
,例如,
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