学习 Kotlin 中构造函数的语法

learn the syntax for constructors in Kotlin

我正在完美地学习 Kotlin 编程语言。我尝试以不同的模式编写代码并尝试理解。但是,我不明白这件事。你能帮我吗? 这是:

open class Parent {
    open val foo = 1
    init {
        println(foo)
    }
}
class Child: Parent() {
    override val foo =2
}
fun main() {
    Child()
}

在此代码中,0 是输出。这将如何?

这是关于构造的 顺序 — 并且是一个很容易上当受骗的微妙陷阱。 (恐怕这个回答有点长,但是这里的问题还是很值得理解的。)

这里有几个基本原则:

  • 超类初始化发生在子类初始化之前。 这包括构造函数中的代码、init 块中的代码和 属性 初始化程序:所有这些都发生在超类之前发生在子类中。

  • 一个 Kotlin 属性 包含一个 getter 方法,一个 setter 方法 (如果它是一个 var)、和支持字段(如果需要)。这就是您可以覆盖属性的原因;这意味着访问器方法被覆盖了。

  • 所有字段在初始化为任何其他值之前最初都保持 0/false/null。 (通常情况下,你不会看到,但这是一种罕见的情况。这与 C 等语言不同,在 C 语言中,如果你没有明确初始化一个字段,它可以保存随机值,具体取决于之前使用的内存对于。)

从第一个原则来看,当你调用Child()构造函数时,它会从调用Parent()构造函数开始。这会将超类的 foo 字段设置为 1,然后获取 foo 属性 并将其打印出来。之后,Child 初始化发生,在本例中只是将其 foo 字段设置为 2.

这里的陷阱是 你实际上有两个 foos!

Parent 定义了一个名为 foo 的 属性,它获取访问器方法和一个支持字段。但是 Child 定义了自己的 属性,称为 foo,覆盖了 Parent 中的那个——那个覆盖了访问器方法,并且也获得了自己的支持字段。

由于该覆盖,当父级的 init 块引用 foo 时,它会调用子级覆盖的 getter 方法,以获取子级支持字段的值。该字段尚未初始化!因此,如上所述,它仍然保持其初始值 0,这是 Child getter returns 的值,因此是 Parent 构造函数打印出的值。

所以这里真正的问题是您在初始化之前访问子类字段。这个问题说明了为什么这是一个非常糟糕的主意!作为一般规则:

A constructor/initialiser 不应访问可能被子类覆盖的方法或 属性。

IDE 可以帮助您:如果您将代码放入 IntelliJ,您会看到 foo 的用法标有警告“Accessing non-final property foo in constructor” .那就是告诉你这种问题是可能的。

当然,还有一些更微妙的情况 IDE 可能无法警告您,例如构造函数调用非开放方法调用开放方法。所以需要小心。

有时您可能需要打破该规则 — 但这种情况非常罕见,您应该非常仔细地检查以确保不会出错(即使后来有人出现并创建了一个新的子类)。你应该在 comments/documentation 中非常清楚地说明发生了什么以及为什么需要它。

请通过以下几点:

  1. 初始化程序块 init {} 块在实例初始化期间被调用。它们以主构造函数命名。

  2. 在上面的代码中,println(foo) 被放置在 init 块内。
    因此,打印的值 i.e. 0 在这种情况下,是赋值语句之前的值 open val foo = 1.

  3. 如果您希望输出为 1,则进行以下更改:

open class Parent {
        open var foo : Int = 0

        init {
            foo = 1
            println(foo)
        }
    }

    class Child: Parent() {
        override var foo =2

    }

    fun main() {
        Child()
    }

  1. 最后,请完成此 post。这将帮助您更好地了解这一领域。

现在,让我们一起 java 了解原因。在 Java 中,不可能覆盖字段,并且在 Kotlin 中是相同的。当您覆盖 属性 时,实际上您覆盖的是 getter,而不是字段。例如,您可以用具有字段的 属性 覆盖没有字段的 属性。那是完全合法的。但是,当来自 superclass 的 属性 和 subclass 中的覆盖 属性 都有字段时,这可能会导致意外结果。让我们看看在我的示例中为 Kotlin class 生成了什么字节码。像往常一样,为了简单起见,我将查看相应的 Java 代码。

public class Parent {
    private final int foo = 1;
    public int getFoo() {return foo;}
    public Parent(){
        System.out.println(getFoo());
    }
}
public final class Child extends Parent {
    private final int foo = 2;
    public int getFoo() {return foo;}
}
public class Main
{
    public static void main (String[] args) {
        new Child();
    }
}

这里注意两点。首先,foo 是微不足道的,所以一个字段和一个 getter 对应于完整的 属性。然后因为 属性 是开放的并且可以在子 class 中被覆盖,它在 class 中的用法被编译为 getter 代码,而不是 field 代码。现在,生成的代码为childclass。注意parent中覆盖的属性class也被编译成一个字段和一个getter,现在是另一个字段。创建 child class 的实例时会发生什么?首先在调用 parent 构造函数时,parent 构造函数初始化第一个 fulfilled with one。但是在 init 部分中,调用了一个覆盖的 getter ,它从 child [=36= 调用 get foo ].因为childclass中的字段还没有初始化,所以返回0。这就是为什么在此处打印 0