Beginner question - Inheritance - why isn't my

2019-01-29 13:10发布

问题:

Noob question here, I am working my way through a Udemy beginner Kotlin course and I can't work out why my age parameter isn't used when I use my derived class, but will work when my base class is used.

Person Class

open class Person(open var firstname: String, open var surname: String,
          open var age: Int) {

val thisyear: Int = Calendar.getInstance().get(Calendar.YEAR)

val dob = thisyear - age

fun printperson() {

    println("$firstname $surname was born in $dob")

}
}

Student Class

class Student(override var firstname: String, override var surname: 
                String, override var age: Int, val studentID: Int):    
                    Person(firstname, surname, age) {

fun returnDetails() {

    println("Hello $firstname, your Student ID is: $studentID")
}
}

Main

fun main(args: Array<String>) {

val studentMike = Student(firstname = "Mike", 
                  surname = "Stand", age = 67, studentID = 8899)

studentMike.printperson()
studentMike.returnDetails()

val personBill = Person(firstname = "Bill", surname = "Hook", age = 34)
personBill.printperson()

}

Output

Mike Stand was born in 2018
Hello Mike, your Student ID is: 8899
Bill Hook was born in 1984

As you can see Bill was a direct use of the method in the Person Class, whereas Mike was an indirect call, the age parameter should have been inherited from the Person Class through the Student Class...

Looking at the official Kotlin docs, it looks like the issue is to do with the "Derived class initialisation order" but for the life of me I can't quite grok how to correct this.

Thanks for your help,

PS apologies for the inaccurate terminology. Always glad of pointers to do better.

回答1:

What Umberto Cozzi said is right, it is to do with the fact that you're referring to an open value in the constructor. If you step through the code you'll see that the sequence of events is:

  1. You call the Student constructor, passing in a value for age.
  2. The first thing that constructor does (before constructing the Student object) is call the superclass's constructor.
  3. During the construction of the superclass (Person) you hit this line of code: val dob = thisyear - age. age is an open member (i.e. overridden in the subclass, Student). So the value should be retrieved from Student. The problem is that at this point the Student hasn't been fully constructed yet (remember the first thing its constructor did was call the superclass's constructor). So it's not possible yet for Person to ask Student what value age should be, as that value hasn't been set yet. If you step through the code and check out the value of age at this line of code, it's zero (the default value for an Int).

So the question is what to do? And I think the thing you should ask is: why does Student override age (and indeed firstname and surname). Is there any different in behaviour of age, firstname and surname between Person and Student? The answer is probably no. So Student shouldn't override these properties: instead it should declare them simply as constructor parameters (without val or var) and pass those values to the base class. In other words, Student should look as follows:

class Student(firstname: String, surname: String, age: Int, val studentID: Int) :
        Person(firstname, surname, age) {
    ...

You might also want to be aware of the fact that your line of code that declares thisyear actually creates a property of Person, called thisyear, which I guess you don't want. Any val or var members that are declared directly in the class (rather than in a function) are a declaration of a property (and this is why this is all being calculated immediately during the construction of the Person object). So you might well want to inline this as:

val dob = Calendar.getInstance().get(Calendar.YEAR) - age

If the calculation is more complex and requires more lines of code, just create a private method (e.g. calculateDob) and call that, e.g. val dob = calculateDob(age)

There's also the slight anomaly that age is a var (i.e. can change) whereas dob is a val (i.e. can't change). So a user could change the value of age, but dob won't be updated. One possible way to solve this is to change dob so that instead of it being a property which is assigned a (read-only) value during construction, make it a property with a getter which will calculate the value every time it's called, e.g.

val dob
    get() = Calendar.getInstance().get(Calendar.YEAR) - age

Another option is to add a backing field and getter/setter for age and update dob whenever age is updated.



回答2:

the documentation you are referring to, says explicitly "When designing a base class, you should therefore avoid using open members in the constructors, property initializers, and init blocks"

this is exactly what is happening in your example. In the base class Person you use the open member age in the constructor (the line where you calculate the dob). This should be avoided because when that line calculating dob is executed, age did not yet receive a value from the derived class.

Ok, I did not answer at the "How to correct this?" but I hope it helps in clarifying what is going on



标签: kotlin