【问题标题】:learn the syntax for constructors in Kotlin学习 Kotlin 中构造函数的语法
【发布时间】:2021-01-23 11:16:13
【问题描述】:

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

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

在此代码中,0 是输出。这会怎么样?

【问题讨论】:

    标签: oop kotlin extends


    【解决方案1】:

    这是关于构造的顺序 - 并且是一个很容易成为牺牲品的微妙陷阱。 (这个答案恐怕有点长,但这里的问题很值得理解。)

    这里有一些基本原则相互冲突:

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

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

    • 所有字段在初始化为任何其他值之前最初都包含0/false/null。 (通常,您不会看到这一点,但这是极少数情况之一。这与 C 等语言不同,在 C 语言中,如果您不显式初始化一个字段,它可以根据该内存先前使用的内容保存随机值为。)

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

    这里的问题是你实际上有两个foos

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

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

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

    构造函数/初始化器不应该访问可能被子类覆盖的方法或属性。

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

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

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

    【讨论】:

      【解决方案2】:

      现在,让我们用 java 来理解为什么。在 Java 中,不可能覆盖字段,而在 Kotlin 的底层也是一样的。实际上,当您覆盖一个属性时,您覆盖的是一个 getter,而不是一个字段。例如,您可以用具有字段的属性覆盖没有字段的属性。这是完全合法的。但是,当超类中的属性和子类中的覆盖属性都具有字段时,可能会导致意外结果。让我们看看在我的示例中为 Kotlin 类生成了哪些字节码。像往常一样,为了简单起见,我将查看相应的 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 get 是微不足道的,所以一个字段和一个 getter 对应于完整的属性。然后因为该属性是开放的并且可以在子类中被覆盖,所以它在类中的使用被编译为 getter 代码,而不是 field 代码。现在,为子类生成代码。注意,父类中被覆盖的属性也被编译为一个字段和一个getter,现在它是另一个字段。当您创建子类的实例时会发生什么?首先在父构造函数被调用时,父构造函数初始化第一个实现的。但是在 init 部分中,调用了一个覆盖的 getter,它从子类调用 get foo。因为子类中的字段还没有初始化,所以返回0。这就是此处打印 0 的原因。

      【讨论】:

        【解决方案3】:

        请通过以下几点:

        1. Initializer Blocks 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。这将帮助您更好地了解该领域。

        【讨论】:

        • @Tenfour04:我在这里不推荐任何东西!我只是想以最简单的方式消除上述问题中的疑虑。
        猜你喜欢
        • 2017-03-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-01-11
        • 2012-06-22
        • 1970-01-01
        • 2018-04-01
        • 1970-01-01
        相关资源
        最近更新 更多