Permalink
120 lines (85 sloc) 5.48 KB

Data Class Inheritance

  • 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.

Feedback

Discussion of this proposal is held in this issue.

Motivation / use cases

Description

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.

Allowing data classes to have base classes

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 vals 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 of equals in the base class makes sense, for example in collections
    • in fact, implementing correct equality in a hierarchy is impossible in general

Member generation rules

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 generate M_g
      • use case: a general toString or equals 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 overriding toString 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
    • if M_b is open, generate M_g to override M_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 the signatures are override-incompatible (e.g. return type of M_b is not a supertype of the return type of M_g)
    • report an error

Examples

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
}

Issues to be fixed

  • 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.

Alternatives

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 whens

Future improvements

  • 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 no val/var parameters after that. This includes varargs and parameters with default values.