Ensuring uniqueness of enum constants

I want to ensure that a certain parameter value is not set twice in the set of enum constants. The best place would be a Set in the companion object to store used values, but the Set property of the companion object would be initialized after the enum constants!

So the best I’ve come up with is a Set outside of the enum in the same file:

private val ids = mutableSetOf<Int>()

enum class Thing(val id: Int) {

    RED(1),
    GREEN(2),
    BLUE(1);

    init {
        require(!ids.contains(id)) { "Duplicate id: $id" }
        ids.add(id)
    }
}

This throws:

Caused by: java.lang.IllegalArgumentException: Duplicate
	at Thing.<init>(Test.kt:16)
	at Thing.<clinit>(Test.kt:13)
	... 2 more

Any better ideas?

1 Like

If this is something you only need once this is probably the best solution. If you need this on lot’s of enums you could write an annotation processor or compiler plugin to do this check at compile time.

3 Likes

If what you want is only an integer, and you don’t care which enum entry is associated with which integer, then you can use ordinal property instead.

Example:

fun main() {

  for (u in Unique.values()) { print(u.ordinal)}

}
​
enum class Unique {
    FIRST,
    SECOND,
    THIRD
    
}

Otherwise, I don’t see any better solution.

2 Likes

My example was a bit simplistic, in fact there is more than one parameter.

Thank you both for your assessment. I will keep as is for now, since it is not worth to develop an annotation processor or compiler plug-in, but I like the idea.

You can write a test:

assertEquals(0, Thing.values()
        .groupBy { it -> it.id }
        .filter { it -> it.value.size > 1 }
        .size)
3 Likes

I second using a unit test in this case, as it avoids any runtime overhead. I do suggest using a library like AssertJ or assertk so that you write assertThat(Thing.values().groupBy { it.id }.filter { it.value.size > 1 }).isEmpty(), which will give you a useful error message of exactly which ids are used multiple times, and which enum values have each of those ids.

3 Likes

I come across this quite often. I use unit tests, but it’s a bit clumsy, and you can easily forget to do it. I wish I could write something like:

enum class MyEnum(unique /* <- !! */ val id: Int) {
   FIRST(10), SECOND(3), THIRD(77)
}

The benefit would be that I would immediately see a compile time error if I try to add a FOURTH(3) with a value that has already been used.

This is a tiny bit clunky, but it does the trick:

abstract class UniqueEnum<E>(val entries: List<E>) {
    abstract val E.field: Any?
    init {
        require(entries.distinctBy { it.field }.size == entries.size)
    }
}

enum class Thing(val id: Int) {
    RED(1),
    GREEN(2),
    BLUE(1);
    companion object: UniqueEnum<Thing>(entries) {
        override val Thing.field get() = id
    }
}
1 Like

@kyay10 Interesting idea to derive the companion object from a class. However, this will create a runtime error, and only when Thing is actually being used, so I’d definitely prefer a unit test. My suggested new syntax would do the uniqueness check at compile time.

1 Like

That would be a good fit for a compiler plug-in!

1 Like