frank_neff
frank_neff

Reputation: 1012

Why is a Kotlin enum property not a compile time constant?

The primary target of this question is understanding the implementation and why it is like this. A solution or workaround for it would of course also be highly appreciated...

Given this example:

enum class SomeEnum(val customProp: String) {
  FOO("fooProp"),
  BAR("barProp");
}

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
annotation class TheAnnotation(
  val targetValue: String
)

@TheAnnotation(targetValue = SomeEnum.FOO.customProp)
fun testFun() {
  
}

The compilation results in the following error:

SomeEnum.kt: (14, 30): An annotation argument must be a compile-time constant

For obvious reasons, annotation values (along with others) must be compile-time constants, which makes sense in many different ways. What is unclear to me, is why customProp is not treated as a constant by the compiler.

If enums are defined as finite, closed sets of information, they should, in my understanding, only be mutable at compile-time a.k.a. "compile-time constant". For the unlikely case that enums somehow are modifiable at runtime in Kotlin, that would answer the question as well.

Addendum:

The enum value (e.g. SomeEnum.FOO) is actually treated as a compile-time constant. The proof is, that the following slightly changed snippet compiles:

enum class SomeEnum(val customProp: String) {
  FOO("fooProp"),
  BAR("barProp");
}

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class TheAnnotation(
  val targetValue: SomeEnum
)

@TheAnnotation(targetValue = SomeEnum.FOO)
fun testFun() {

}

Upvotes: 3

Views: 1844

Answers (1)

Sweeper
Sweeper

Reputation: 271705

enums are defined as finite, closed sets of information, they should, in my understanding, only be mutable at compile-time

Actually, no. An enum class is just a special kind of class, that doesn't allow you to create any new instances other than the ones that you name in the declaration, plus a bunch more syntactic sugars. Therefore, like a regular class, it can have properties whose values are only known at runtime, and properties that are mutable (though this is usually a very bad idea).

For example:

enum class Foo {
    A, B;

    val foo = System.currentTimeMillis() // you can't know this at compile time!
}

This basically de-sugars into:

class Foo private constructor(){
    val foo = System.currentTimeMillis()
    companion object {
        val A = Foo()
        val B = Foo()
    }
}

(The actual generated code has a bit more things than this, but this is enough to illustrate my point)

A and B are just two (and the only two) instances of Foo. It should be obvious that Foo.A is not a compile time constant*, let alone Foo.A.foo. You could add an init block in Foo to run arbitrary code. You could even make foo a var, allowing you to do hideous things such as:

Foo.A.foo = 1
// now every other code that uses Foo.A.foo will see "1" as its value

You might also wonder why they didn't implement a more restricted enum that doesn't allow you to do these things, and is a compile time constant, but that is a different question.

See also: The language spec


* Though you can still pass Foo.A to an annotation. To an annotation, Foo.A is a compile time constant, because all the annotation has to do, is to store the name "Foo.A", not the object that it refers to, which has to be computed at runtime.

Upvotes: 5

Related Questions