Kotlin 中 class 中的初始块位置

init block position in class in Kotlin

我最近遇到了这样一种情况,即使我已经使用 init 块通过构造函数分配了一个值,但我的标准变量的值被默认值替换了。

我试过的是:

class Example(function: Example.() -> Unit) {

    init {
        function()
    }

    var name = "default name"

}


// assigning it like this:
val example = Example { name = "new name" }

// print value
print(example.name)  // prints "default name"

经过一番努力,我发现初始化块的位置很重要。如果我将 init 块放在 class 的最后,它会首先使用默认名称初始化名称,然后调用 function() 将值替换为 "new name".

如果我把它放在第一位,它找不到名称,它会在属性初始化时被 "default name" 替换。

这对我来说很奇怪。谁能解释为什么会这样?

原因是 kotlin 遵循 自上而下 方法

来自文档 (An in-depth look at Kotlin’s initializers) 初始化程序(属性 初始化程序和 init 块)按照它们在 class、top 中定义的顺序执行-到底部.

您可以定义多个辅助构造函数,但在创建 class 实例时只会调用一个,除非该构造函数显式调用另一个。

构造函数也可以有默认参数值,每次调用构造函数时都会计算这些值。与 属性 初始值设定项一样,这些可以是函数调用或其他将 运行 任意代码的表达式。

初始化器 运行 在 class 主构造函数的开头从上到下。

这是正确的方法

class Example(function: Example.() -> Unit) {
var name = "default name"
init {
    function()
}
}

如 Kotlin 文档中所述:

During an instance initialization, the initializer blocks are executed in the same order as they appear in the class body, interleaved with the property initializers: ...

https://kotlinlang.org/docs/classes.html#constructors

Java 构造函数只是在对象创建后 运行 的一个方法。在 运行 构建构造函数之前,所有 class 字段都被初始化。

在 Kotlin 中有两种类型的构造函数,即 主构造函数 辅助构造函数。我将主构造函数视为支持字段封装 built-in 的常规 java 构造函数。编译后,如果主构造函数字段已声明对整个 class.

可见,则它们将放在 class 的顶部

在java或kotlin中,构造函数在初始化class字段后被调用。但是在主构造函数中我们不能写任何语句。如果我们要编写需要在对象创建后执行的语句,我们必须将它们放在初始化块中。但是 init 块是按照它们出现在 class 主体中的方式执行的。我们可以在 class 中定义多个 init 块。它们将从上到下执行。

让我们用 init 块做一些实验..

Test.kt

fun main() {
    Subject("a1")
}

class Element {

    init {
        println("Element init block 1")
    }

    constructor(message: String) {
        println(message)
    }

    init {
        println("Element init block 2")
    }

}

class Subject(private val name: String, e: Element = Element("$name: first element")) {

    private val field1: Int = 1

    init {
        println("$name: first init")
    }

    val e2 = Element("$name: second element")

    init {
        println("$name: second init")
    }

    val e3 = Element("$name: third element")

}

让我们编译上面的代码并 运行 它。

kotlinc Test.kt -include-runtime -d Test.jar
java -jar Test.jar

以上程序的输出为

Element init block 1
Element init block 2
a1: first element
a1: first init
Element init block 1
Element init block 2
a1: second element
a1: second init
Element init block 1
Element init block 2
a1: third element

如你所见,第一个主构造函数被调用,在次构造函数之前,所有的初始化块都被执行。这是因为 init 块按照它们在 class 主体中出现的顺序成为构造函数的一部分。

让我们将 kotlin 代码编译为 java 字节代码并将其反编译回 java。我用jd-gui反编译了javaclasses。您可以在基于 arch linux 的发行版中使用 yay -S jd-gui-bin 安装它。

这是我反编译Subject.class文件后得到的输出

import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(mv = {1, 6, 0}, k = 1, xi = 48, d1 = {"[=13=]04\n[=13=]20[=13=]2\n[=13=]20[=13=]0\n[=13=]0\n[=13=]206\n[=13=]0\n[=13=]20[=13=]2\n[=13=]2\b[=13=]7\n[=13=]20\b0[=13=]02[=13=]20[=13=]1B72[=13=]60[=13=]22[=13=]20[=13=]32\b\b[=13=]20[=13=]42[=13=]20[=13=]5[=13=]6[=13=]20[=13=]6R10[=13=]72[=13=]20[=13=]5[=13=]6\b\n[=13=]02[=13=]4\b\b0\tR10\n2[=13=]20[=13=]5[=13=]6\b\n[=13=]02[=13=]4\b30\tR60\f2[=13=]20\rX[=13=]6[=13=]2\n[=13=]0R60[=13=]22[=13=]20[=13=]3X[=13=]4[=13=]6[=13=]2\n[=13=]0"}, d2 = {"LSubject;", "", "name", "", "e", "LElement;", "(Ljava/lang/String;LElement;)V", "e2", "getE2", "()LElement;", "e3", "getE3", "field1", ""})
public final class Subject {
  @NotNull
  private final String name;
  
  private final int field1;
  
  @NotNull
  private final Element e2;
  
  @NotNull
  private final Element e3;
  
  public Subject(@NotNull String name, @NotNull Element e) {
    this.name = name;
    this.field1 = 1;
    System.out
      .println(Intrinsics.stringPlus(this.name, ": first init"));
    this.e2 = new Element(Intrinsics.stringPlus(this.name, ": second element"));
    System.out
      .println(Intrinsics.stringPlus(this.name, ": second init"));
    this.e3 = new Element(Intrinsics.stringPlus(this.name, ": third element"));
  }
  
  @NotNull
  public final Element getE2() {
    return this.e2;
  }
  
  @NotNull
  public final Element getE3() {
    return this.e3;
  }
}

如您所见,所有 init 块都按照它们在 class 主体中出现的顺序成为构造函数的一部分。我注意到一件事与 java 不同。 Class 字段已在构造函数中初始化。 Class 字段和 init 块按照它们在 class 正文中出现的顺序进行初始化。看来顺序在 kotlin 中是如此重要。