Search code examples
kotlininheritanceimmutabilitymutability

Changing a mutable value in a class inheriting from a sealed class with an immutable value


In Kotlin, I'm trying to have a mutable generic value in sealed class A, and a mutable generic Number value in sealed class B with a mutable Long/... value in final C/...; but whenever I change A and B's values to "var" it gives me: Var-property type is T, which is not a type of overridden public open var value: Number.

Example:

sealed class Data<T>(
    open val name: String,
    open var value: T
)

sealed class NumberData<T: Number>(
    override val name: String,
    override var value: T
): Data<Number>(name, value)

class TextData(name: String, override var value: String) : Data<String>(name, value)
class LongData(name: String, override var value: Long) : NumberData<Long>(name, value)
// class ...Data(name: String, value: ...) : Data<...>(name, value)

fun main() {
    val dataSet = setOf<Data<*>>(TextData("a1", "hello, world"), LongData("a2", 50024))
    val translate = "a1 a2 c3"

    // Goal: Translate "a1 a2 c3" into a string using dataSet values while ignoring those that aren't in the set.
    // "a1 a2 c3" -> "a1's value a2's value"

    buildSet {
        val map = dataSet.associateBy { it.name }
        for(name in translate.split(" ")) {
            if(map.containsKey(name)) {
                add(map[name]!!.apply { value = 5 }) // "val" cannot be reassigned
                // cannot change val to var in Data as NumberData will give a compiler error:
                // Var-property type is T, which is not a type of overridden public open var value: Number
            }
        }
    }.joinToString(" ") { it.value.toString() }
}

Why is this, and how would I go about fixing it?


Solution

  • NumberData<T> inherits from Data<Number>, not Data<T>, so the type of value is expected to be Number, not T.

    It is possible to override a read-only property with a subtype, e.g.

    open class Foo(
        open val x: Number
    )
    
    open class Bar(
        override val x: Int
    ): Foo(x)
    

    This is the case when you do override val value: T. T is a subtype of Number.

    This is because someone accessing an instance of Bar via Foo can still access x without breaking anything.

    val f: Foo = Bar(100)
    val number: Number = f.x // 100 is a Number, so everything is fine
    

    However, if x can be set, this would break:

    val f: Foo = Bar(100)
    // Foo.x declares a setter that takes a Number. Double is a Number, so this should be possible
    // But what this actually does though, is that it sets Bar.x at runtime, which is an Int!
    f.x = 2.5
    

    You probably meant for NumberData<T> to inherit Data<T> instead.


    It is also not possible to set the value of a Data<*>. This is because you don't know what the exact type of Data it is. Is it a Data<String>? Data<Long>? Or Data<SomethingElse>? If you don't know that, how would you know that value can take the value 5?

    To do what you're trying to do, you don't need to set the value at all. You can just do:

    val map = dataSet.associate { it.name to it.value }
    val result = translate
        .split(" ")
        .mapNotNull { map[it]?.toString() }
        .joinToString(" ")
    println(result)