3

I have 2 enums that cross-reference each other

enum class School(val zone: String, val captain: Student) {
    Metricon("New York", Student.David),
    Carlisle("London", Student.Paul)
}

enum class Student(val lastName: String, val school: School) {
    David("Samuel", School.Metricon),
    Solomon("Handsome", School.Metricon),
    Saul("Black", School.Metricon),
    Paul("Lewis", School.Carlisle),
    Joseph("Hardy", School.Carlisle),
    John("Baptise", School.Carlisle),
}

When I print it out

        val student = Student.David
        val school = School.Carlisle

        println(student.school)
        println(school.captain)
        println(student.lastName)
        println(school.zone)

It resulted in

Metricon
null
Samuel
London

Notice the null there. The school.captain is missing and became null.

How can I solve this problem?

Elye
  • 53,639
  • 54
  • 212
  • 474
  • Does this answer your question? [Java Enums: Two enum types, each containing references to each other?](https://stackoverflow.com/questions/1506410/java-enums-two-enum-types-each-containing-references-to-each-other) – Ivo Aug 12 '22 at 08:38
  • @Ivo I don't think it is a duplicate of that, as OP isn't specifically asking about Kotlin/JVM, and this code is problematic across all platforms. It even throws an exception on Kotlin/Native. – Sweeper Aug 12 '22 at 08:48
  • @Sweeper ah yeah, you're right. I didn't think about other platforms – Ivo Aug 12 '22 at 08:50

1 Answers1

3

The reason for this is specified in the Kotlin Language Specification - Classifier Initialisation, after talking about the exact steps in which initialisation takes place,

If any step in the initialization order creates a loop, it results in unspecified behaviour.

When initialising any of the Student enum entries, a School enum entry would need to initialised (as you used those as arguments for the constructor), and when initialising a School, a Student needs to be initialised (for the same reason), hence forming a loop. According to the spec, this is unspecified behaviour, and each platform can do its own thing.

There are many ways around this.

One way could be to put the enum parameters into lambdas so that they are lazily evaluated:

enum class School(val zone: String, private val _captain: () -> Student) {
    Metricon("New York", { Student.David }),
    Carlisle("London", { Student.Paul });

    val captain get() = _captain()
}

enum class Student(val lastName: String, private val _school: () -> School) {
    David("Samuel", { School.Metricon }),
    Solomon("Handsome", { School.Metricon }),
    Saul("Black", { School.Metricon }),
    Paul("Lewis", { School.Carlisle }),
    Joseph("Hardy", { School.Carlisle }),
    John("Baptise", { School.Carlisle });

    val school get() = _school()
}

Another idea would be to use an abstract val instead, but this is more verbose:

enum class School(val zone: String) {
    Metricon("New York") { override val captain get() = Student.David },
    Carlisle("London") { override val captain get() = Student.Paul };

    abstract val captain: Student
}

enum class Student(val lastName: String) {
    David("Samuel") { override val school get() = School.Metricon},
    Solomon("Handsome") { override val school get() = School.Metricon },
    Saul("Black") { override val school get() = School.Metricon },
    Paul("Lewis") { override val school get() = School.Carlisle },
    Joseph("Hardy") { override val school get() = School.Carlisle },
    John("Baptise") { override val school get() = School.Carlisle };

    abstract val school: School
}

Finally, for your specific situation, you can also change your design to include a isCaptain flag in the Students instead.

enum class Student(val lastName: String, val school: School, val isCaptain: Boolean = false) {
    David("Samuel", School.Metricon, isCaptain = true),
    Solomon("Handsome", School.Metricon),
    Saul("Black", School.Metricon),
    Paul("Lewis", School.Carlisle, isCaptain = true),
    Joseph("Hardy", School.Carlisle),
    John("Baptise", School.Carlisle),
}

Then in you can do a linear search to find out who's captain.

// in School
val captain: Student get() = Student.values().first { it.school == this && it.isCaptain }

Side note: Are you trying to create a database using enum entries? Enums are not the suitable tool for this. Use an actual database instead.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Good answer, but I think it's worth mentioning that using enums as a database is not at all scalable. If you add one more student, it will break backward compatibility. The solution should use Maps instead (or even better, a database). I know it's just a learning exercise, but why learn to use enums for something they're not designed to do? – Klitos Kyriacou Aug 12 '22 at 09:31
  • 1
    @KlitosKyriacou I agree that you shouldn't use enums to create a database-like thing. But I do see some legitimate uses of enums where circular references would occur. I remember seeing a [Java question](https://stackoverflow.com/q/60372779/5133585) about making a `Direction` enum with the 4 cardinal directions, and the asker wanted a `opposite` field that stores the opposite direction. – Sweeper Aug 12 '22 at 09:43