Kotlin 类中的 init 块位置

Posted

技术标签:

【中文标题】Kotlin 类中的 init 块位置【英文标题】:init block position in class in Kotlin 【发布时间】:2018-05-06 23:13:22 【问题描述】:

我最近遇到了一种情况,即使我使用 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 块的位置很重要。如果我将 init 块放在类的最后,它首先用默认值初始化名称,然后调用 function() 将值替换为“新名称”。

如果我把它放在第一位,它没有找到名称,并且在初始化属性时被“默认名称”替换。

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

【问题讨论】:

类初始化是自上而下的,初始化块没有被豁免。 这个练习仅仅是为了理解 Kotlin 初始化吗?如果没有,您可以通过class Example(val name: String = "default name") 实现此目的 我试图在我的库 (github.com/kirtan403/k4kotlin) 中执行此操作,但在将其放在顶部之后,整个功能都崩溃了。 签出这个???? init-blocks kotlin vs Java我解释了什么是init块以及它的调用顺序是如何初始化块和全局变量 【参考方案1】:

原因是kotlin遵循从上到下的方法

根据文档 (An in-depth look at Kotlin’s initializers) 初始化程序(属性初始化程序和初始化块)按照它们在类中定义的顺序执行,从上到下

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

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

初始化程序在类的主构造函数的开头从上到下运行。

这是正确的方法

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


【讨论】:

在大多数情况下将 init 块放在底部是什么意思?除非你有一些特殊的需求......【参考方案2】:

如 Kotlin 文档中所述:

在实例初始化期间,初始化程序块按照它们出现在类主体中的相同顺序执行,并与属性交错初始化器:...

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

【讨论】:

【参考方案3】:

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

在 Kotlin 中有两种类型的构造函数,即主构造函数次构造函数。我将主构造函数视为支持内置字段封装的常规 Java 构造函数。编译后,如果声明对整个类可见,主构造函数字段将放在类的顶部。

在 java 或 kotlin 中,构造函数在初始化类字段后被调用。但是在主构造函数中我们不能写任何语句。如果我们要编写在对象创建后需要执行的语句,我们必须将它们放在初始化块中。但是 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 块都被执行。这是因为 init 块按照它们在类体中出现的顺序成为构造函数的一部分。

让我们将 kotlin 代码编译为 java 字节码并将其反编译回 java。我使用jd-gui 来反编译java 类。您可以在基于 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 = "\000\034\n\002\030\002\n\002\020\000\n\000\n\002\020\016\n\000\n\002\030\002\n\002\b\007\n\002\020\b\030\0002\0020\001B\027\022\006\020\002\032\0020\003\022\b\b\002\020\004\032\0020\005\006\002\020\006R\021\020\007\032\0020\005\006\b\n\000\032\004\b\b\020\tR\021\020\n\032\0020\005\006\b\n\000\032\004\b\013\020\tR\016\020\f\032\0020\rX\006\002\n\000R\016\020\002\032\0020\003X\004\006\002\n\000", 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 块都按照它们在类主体中出现的顺序成为构造函数的一部分。我注意到与java不同的一件事。类字段在构造函数中初始化。类字段和初始化块按照它们在类主体中出现的顺序进行初始化。似乎顺序在 kotlin 中非常重要。

【讨论】:

以上是关于Kotlin 类中的 init 块位置的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin类的初始化 ③ ( init 初始化块 | 初始化顺序 : 主构造函数属性赋值 -> 类属性赋值 -> init 初始化块代码 -> 次构造函数代码 )

Kotlin init 小记

Kotlin init 小记

Kotlin之初始化

代码块{}

Java 类中各成分加载顺序 和 内存中的存放位置