- Type: Design proposal
- Author: Alexander Udalov
- Status: Accepted
- Prototype: Implemented in Kotlin 1.1
Goal: allow data
classes to inherit from other (non-final) classes.
Discussion of this proposal is held in this issue.
-
Algebraic data types, or (in Kotlin terms) sealed hierarchies:
sealed class Something { object Singleton : Something() data class Complex(...) : Something() // <-- ! }
https://discuss.kotlinlang.org/t/sealed-classes-design/82 http://stackoverflow.com/questions/35921234/kotlin-sealed-class-cannot-contain-data-classes-why
Data classes should be allowed to have base classes. Below we explain why it's safe, and what are the exact rules to generate special members in such data classes.
Note that any data class itself remains to be final and so cannot be used as a base class.
It seems that there has not been much profit in prohibiting data classes with superclasses. It only helped to reduce confusion in some cases:
- if the superclass has its own primary constructor, it may be confusing because its
val
s are not considered the components of the data class- we could restrict primary constructors in base classes, but it would still be possible to create properties in the body of a class
- if the superclass has
equals
/hashCode
, it may be confused with data class'equals
/hashCode
- restricting custom
equals
in the base class breaks a use case when a general implementation ofequals
in the base class makes sense, for example in collections - in fact, implementing correct equality in a hierarchy is impossible in general
- restricting custom
The main effect of the data
modifier on a class is special members generated by the compiler: equals
, hashCode
, toString
, copy
and componentN
functions.
In the case when the base class of a data class has a member M_b
that may clash with a generated member M_g
, we should behave as follows:
- if the signatures are override-compatible
- if
M_b
is final, don't generateM_g
- use case: a general
toString
orequals
in the base class that uses the public API to render or compare instances - use case:
toString
of the sealed class wants to do case-analysis on the type of this instead of overridingtoString
in each subclass - downside: for both of these cases there can't be a partial behavior: either all data-subclasses override a member of the base class, or none do
- use case: a general
- if
M_b
is open, generateM_g
to overrideM_b
without notice- use case: base class defines component1, 2, 3 to express that any of its cases can be decomposed into (at least) three components, but implementations of components are different in subclasses/subclasses rely on data to implement component functions
- downside: if the user is not aware of some members being generated, they'd be surprised to get such an override
- if
- if the signatures are override-incompatible (e.g. return type of
M_b
is not a supertype of the return type ofM_g
)- report an error
Sealed hierarchy use case:
sealed class Either<out L, out R> {
data class Left<out L, out R>(val value: L) : Either<L, R>()
data class Right<out L, out R>(val value: R) : Either<L, R>()
}
Generation of special members in cases when members are present in the base class:
open class Base {
override /* open */ fun hashCode() = 42
override final fun toString() = "Base"
}
data class Derived(val value: String): Base()
fun test() {
val d = Derived("Derived")
println(d) // prints "Base"
println(d.hashCode()) // prints hashCode computed in Derived, NOT 42
println(d == Derived("Derived")) // prints true
}
Properties of base class' primary constructor do not participate in componentN
and other special functions:
open class Base(val baseParam: Int)
data class Derived(val dataParam: String) : Base(dataParam.length)
fun test() {
val d = Derived("OK")
val x = d.component1() // x is String and its value is "OK"
val (y) = d // similarly, y is "OK"
val (a, b) = d // error, no component2 in Derived
}
- KT-10330. The original issue with the sealed hierarchy use case.
- KT-11306.
Unless this issue is fixed, it's not possible to specify for example abstract
toString
in the base class and rely on auto-generated members in data subclasses. This can hurt in sealed hierarchies where some of the subclasses are data classes and some are not.
To support exactly the sealed hierarchy use case, we could allow sealed interfaces (data classes can inherit from interfaces). However:
- it's not possible to store data in an interface
- it's possible to accidentally inherit such interface from Java, which would break exhaustive
when
s
- Similarly to the base class restriction, disallowing non-
val
/var
constructor parameters for data classes seems pretty harsh. It looks like such parameters could be allowed at least if they are the last ones, i.e. there are noval
/var
parameters after that. This includesvararg
s and parameters with default values.