Don’t use var in enums

Lukas Vyletel
4 min readDec 16, 2023

--

Photo by Kenny Eliason on Unsplash

Recently I came across this kind of code.

enum class SomeEnum(var someValue: String) {
TEST1("test 1"),
TEST2("test 2")
}

Notice the usage of var inside the enum cases. This is actually a very bad idea. Depending on the usage of the enum you may never notice in your normal development process that this kind of pattern introduces a subtle bug into your application.

To illustrate what is wrong with this kind of pattern, let’s take a look at how this code behaves in unit tests:

class SomeEnumTest {

@Test
fun test1() {
SomeEnum.TEST1.someValue = "random"
assertEquals("test 2", SomeEnum.TEST2.someValue)
}

@Test
fun test2() {
assertEquals("test 1", SomeEnum.TEST1.someValue)
}
}

If you now run all the tests in the SomeTest class, despite the fact that the test2 test case does not overwrite the value of SomeEnum.TEST1 case, the test2 test case will fail — maybe. Whether it fails or not depends solely on the order of execution of the test cases. Let’s stick for now with the assumption that the test1 test case runs first and the test2 test case runs second. In this scenario the test2 test case will fail.

This is a problem. test2 was affected by behavior of test1. However, to be able to test that your code behaves properly, one test case should not affect other test cases! Depending on naming of your test cases or other rules of execution of unit tests you might not even notice, that there’s a problem in your implementation. You might get into a situation where a test case passes despite the fact that it should fail!

But why is the test2 even failing in the first place?

The reason for that is, that enum cases are initialized only once at the start of the application. When overwriting value of a var inside an enum case, all the usages of the enum case are affected. enum cases effectively act as object in Kotlin.

In fact, should you try writing similar pattern with objects , you’d see the exact same behavior:

class SomeObjectTest {

@Test
fun test1() {
SomeObject.someValue = "random"
assertEquals("random", SomeObject.someValue)
}

@Test
fun test2() {
assertEquals("someValue", SomeObject.someValue)
}
}
Photo by Eric Ward on Unsplash

So is this a problem only about testing?

Absolutely not! Even your runtime code is affected. I have shown you the unit tests only to demonstrate the problem that affects the runtime of your application as well. Specifically, if an enum or an object are reused by different parts of the application, overwriting their var value in one place will have negative aspects on other parts of the application that are using them. This problem lies at the crux of the the infamous singleton pattern/anti-pattern debate.

So how do I avoid this problem?

First of all NEVER use var inside enum cases or inside an object. If you see anybody using a var inside an enum or an object, send them here.

In fact it would be very beneficial to avoid using var altogether in your code! Immutable code is the safest bet to avoid weird runtime bugs.

Having said that, let’s say that for some obscure reason, you absolutely do have to use var , you do have to overwrite a value of an enum case (ask yourself if you really absolutely must use var). What to do then?

In that case do not use enum ! Fortunately, Kotlin is your friend here, providing you with an alternate solution. sealed class or a sealed interface .

sealed class SomeSealedClass(var someValue: String) {
class Test1 : SomeSealedClass("test 1")
class Test2 : SomeSealedClass("test 2")
}

class SomeSealedClassTest {

@Test
fun test1() {
SomeSealedClass.Test1().someValue = "random"
assertEquals("test 2", SomeSealedClass.Test2().someValue)
}

@Test
fun test2() {
assertEquals("test 1", SomeSealedClass.Test1().someValue)
}
}

These test cases do not fail regardless of execution order, because as you can notice, we’re creating a completely new instance of Test1 in both test cases. These are not shared across the execution of the whole application.

Having said that, after reading this article, you should already know, how the following code would behave:

sealed class SomeSealedClass(var someValue: String) {
object Test1 : SomeSealedClass("test 1")
object Test2 : SomeSealedClass("test 2")
}

class SomeSealedClassTest {

@Test
fun test1() {
SomeSealedClass.Test1.someValue = "random"
assertEquals("test 2", SomeSealedClass.Test2.someValue)
}

@Test
fun test2() {
assertEquals("test 1", SomeSealedClass.Test1.someValue)
}
}

Depending on the order of execution, the test2 test case fill of course fail, because now we’re using a var inside an object (both Test1 and Test2 are objects) and the instance is shared across the whole run of the application.

I hope it is now clear, why you should avoid var s inside enum or object s like cancer.

--

--

Responses (1)