14

How can I create an enum class with circular references?

Simple example (taken from this Java question):

enum class Hand(val beats: Hand) {
    ROCK(SCISSORS), // Enum entry 'SCISSORS' is uninitialized here
    PAPER(ROCK),
    SCISSORS(PAPER);
}
Michael Hoff
  • 6,119
  • 1
  • 14
  • 38

2 Answers2

16

As reassignment is forbidden for val properties, this problem is generally hard to solve and often indicates problems in your data model. For a discussion in broader context refer to this question/answer(s).

However, this simple example can be solved using a val property with custom getter (thus without a backing field). Using when, the getter can be defined in a very readable way:

enum class Hand {
    ROCK,
    PAPER,
    SCISSORS;

    val beats: Hand
        get() = when (this) {
            ROCK -> SCISSORS
            PAPER -> ROCK
            SCISSORS -> PAPER
        }
}

An alternative solution (analogue to the answer by Eugen Pechanec) is to use sealed classes. Due to their less constrained concept, the implementation is1 slightly simpler and more readable compared to an enum with overridden properties.

sealed class Hand {
    abstract val beats: Hand

    object ROCK: Hand() {
        override val beats = SCISSORS
    }

    object PAPER: Hand() {
        override val beats = ROCK
    }

    object SCISSORS: Hand() {
        override val beats = PAPER
    }
}

1personal opinion

Disclaimer: I have no information on how these solutions work in conjunction with classic Java.

Michael Hoff
  • 6,119
  • 1
  • 14
  • 38
  • Hmm, it's more readable because you use a property with a backing field, which will take up a small amount of memory. I could do that too and have implicit `: Hand()` ^^ Other than that these two cases compile down to virtually the same bytecode. – Eugen Pechanec Nov 18 '20 at 16:15
8

An alternative to mhoff's answer without flow control statements:

enum class Hand {
    ROCK {
        override val beats: Hand
            get() = SCISSORS
    },
    PAPER {
        override val beats: Hand
            get() = ROCK
    },
    SCISSORS {
        override val beats: Hand
            get() = PAPER
    };

    abstract val beats: Hand
}
Eugen Pechanec
  • 37,669
  • 7
  • 103
  • 124
  • Nice solution! I wonder if there is a (significant) difference in performance between the two solutions. – Michael Hoff Feb 21 '18 at 15:45
  • I can say that this will definitely take up more space in memory and on disk. It generates a singleton class (extends Hand) for each of ROCK, PAPER, and SCISSORS. On the other hand this should perform faster - you don't do the switch. – Eugen Pechanec Feb 21 '18 at 15:58
  • I have the same problem and I used this solution help me a lot. – Caio Dec 27 '22 at 14:21