【问题标题】:What's an example of using an overridden property in the Base Class initialization (either directly or indirectly)?在基类初始化(直接或间接)中使用覆盖属性的示例是什么?
【发布时间】:2021-07-11 08:23:55
【问题描述】:

这意味着,在基类构造函数执行时,派生类中声明或覆盖的属性尚未初始化。如果在基类初始化逻辑中使用了这些属性中的任何一个(直接或间接地,通过另一个覆盖的开放成员实现),则可能导致不正确的行为或运行时故障。因此,在设计基类时,应避免在构造函数、属性初始化程序和 init 块中使用开放成员。

我正在从 Kotlin 文档中学习继承,但我被困在这里。有另一个帖子问了一个关于这个的问题,但答案只是文档以不同的方式说的。

  • 说清楚,我理解构造函数和继承之间的数据流。我无法理解的是我们如何在基类初始化中使用被覆盖的属性。它说

可能直接或间接发生

  这实际上是什么意思?基类如何能以某种方式访问​​派生类中被覆盖的属性?

  • 另外,它说

因此,您应该避免在构造函数中使用开放成员, 属性初始化器和初始化块。

 那么我们如何正确使用开放属性呢?

编辑评论:

fun main ()
{
    val d = Derived("Test2")
}

open class Base()
{
    open val something:String = "Test1"

    init
    {
        println(something)  //prints null
    }
}

class Derived(override val something: String): Base()

【问题讨论】:

    标签: kotlin


    【解决方案1】:

    这实际上是什么意思?基类如何能以某种方式访问​​派生类中被覆盖的属性?

    这是一种直接的方式:

    abstract class Base {
        abstract val something: String
    
        init {
            println(something)
        }
    }
    
    class Child(override val something: String): Base()
    
    fun main() {
        Child("Test") // prints null! because the property is not initialized yet
    }
    

    这会打印出null,这对于不可为空的String 属性来说是非常糟糕的。

    因此,您应该避免在构造函数、属性初始化器和 init 块中使用开放成员。

    那么我们怎样才能正确使用开放属性呢?

    您可以在基类的常规方法(或自定义属性 getter)中使用这些属性:

    abstract class Base {
        
        abstract val something: String
    
        fun printSomething() {
            println(something)
        }
    }
    
    class Child(override val something: String): Base()
    
    fun main() {
        Child("Test").printSomething() // correctly prints "Test"
    }
    

    编辑:以下是有关 cmets 后续问题的一些说明。

    我不太明白为什么 init 块中的代码会用于子类构造函数中的参数

    我认为您可能会对 Kotlin 的主要构造函数的简洁语法感到困惑,这可能会使调试器的流程难以理解。在Child声明中,我们其实声明了很多东西:

    • 参数something 传递给Child 的主要构造函数
    • Child 类上的 属性something,它覆盖了父类的属性
    • 对父构造函数的调用 (Base())

    Child() 被调用时,它立即调用Base() 无参数构造函数,该构造函数运行init 块。

    我们甚至没有用参数或任何东西委托基本构造函数,但它仍然适用于进行覆盖的参数

    您可能会在此处混合 声明运行时。尽管我们在Base 类和Child 类中声明事物,但在此示例代码中,运行时只有一个实例(Child 的实例)。

    所以,其实这里只有1个属性叫something(内存中只有一个地方)。如果init 块访问此属性,它只能是子实例的属性。我们不需要向Base 构造函数传递任何内容,因为init 块是使用Child 实例的数据/字段有效地执行的。

    如果您看到 Java 等价物,您可能不会那么困惑。很明显,如果您将抽象 something 视为 getter getSomething() 的声明。子类重写这个getSomething() 方法并声明一个私有的something 字段,getter 返回字段something 的当前值。但是该字段仅在父(和 init 块)的构造函数完成执行后初始化。

    【讨论】:

    • 我尝试通过调试第一个示例来跟踪数据。它表明,在您到达 init 块后,它会跳回构造函数(或者可能是类)以获取“某物”的值,并且因为它没有被初始化,所以它给出了 null。首先,我想问一下我是否正确理解了流程,如果是,我不太明白为什么 init 块中的代码会用于子类构造函数中的参数。我们甚至没有用参数或任何东西委托基本构造函数,但它仍然适用于进行覆盖的参数。
    • @JockyCracker 也许你对 Kotlin 的主要构造函数的紧凑语法感到困惑。我编辑了我的答案。
    • 我把术语参数和属性混淆了,mb。我的意思是超类如何能够在 init 块中使用子类的属性。我还尝试了一个非抽象示例,我目前的理解是:我们可以在超类中编写打印“某物”而不会出现任何编译时错误,因为超类本身实际上具有该属性,但是当我们使用“覆盖”时' 在子类中,并创建它的实例,它会覆盖超类中属性的值。这样对吗?我还将添加我创建的用于测试。
    • @JockyCracker 对,Child 实例只有属性的覆盖定义可用,并且只有一个Child 实例。 init 块作为Child 实例初始化的一部分有效地运行,即使它的代码在其他地方定义。你可以把它想象成init 块本身在子实例中,并且首先声明(这就是为什么它看不到something 的初始值)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-12
    相关资源
    最近更新 更多