学习 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.
这里的陷阱是 你实际上有两个 foo
s!
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 中非常清楚地说明发生了什么以及为什么需要它。
请通过以下几点:
初始化程序块 即 init {}
块在实例初始化期间被调用。它们以主构造函数命名。
在上面的代码中,println(foo)
被放置在 init
块内。
因此,打印的值 i.e. 0 在这种情况下,是赋值语句之前的值 open val foo = 1
.
如果您希望输出为 1,则进行以下更改:
open class Parent {
open var foo : Int = 0
init {
foo = 1
println(foo)
}
}
class Child: Parent() {
override var foo =2
}
fun main() {
Child()
}
- 最后,请完成此 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。
我正在完美地学习 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.
这里的陷阱是 你实际上有两个 foo
s!
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 中非常清楚地说明发生了什么以及为什么需要它。
请通过以下几点:
初始化程序块 即
init {}
块在实例初始化期间被调用。它们以主构造函数命名。在上面的代码中,
println(foo)
被放置在init
块内。
因此,打印的值 i.e. 0 在这种情况下,是赋值语句之前的值open val foo = 1
.如果您希望输出为 1,则进行以下更改:
open class Parent {
open var foo : Int = 0
init {
foo = 1
println(foo)
}
}
class Child: Parent() {
override var foo =2
}
fun main() {
Child()
}
- 最后,请完成此 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。