0
votes

TL;DR

The Kotlin compiler gives an error (type mismatch) here:

fun <T: A()> getUtil(t: T): Util<T> = if (t is B) UtilB() else // ...

With signature of class B being: class B : A(), class Util being class Util<T: A> and class UtilB being class UtilB: Util<B>().

The Kotlin compiler gives a warning (unchecked cast) here:

fun <T: A()> getUtil(t: T): Util<T> = if (t is B) UtilB() as Util<T> else // ...

To my understanding Kotlin smart cast should know that UtilB() as Util<T> is checked by t is B.

Java code and compiler give the exact same result.

As far as I know this must be a limitation on Java generics. How can I fix this?

Problem description

I have the following setup where an abstract class has multiple implementations and a util class that provides the same functionality for each of those implementations.

To be type safe I figured I would create an abstract class Util<T: A> and for each derived class of A another UtilB: Util<B> class.

To get the right util class for each implementation, I created a function on a companion object getUtil which returns the correct util class for each implementation based on a parameter of generic type T which extends A: T: A thus having return type Util<T>.

However, when I wrote the function body for each derived class of A by checking the type of the parameter with is B and then returning the correct util with UtilB(), the Kotlin compiler gave me an error at the return point saying that UtilB isn't of type Util<T> even though it should be.

I then casted UtilB to Util<B> and that worked but gave me an error "Unchecked cast". According to my understanding Kotlin smart cast should be able to figure out that it is indeed a valid checked cast (checked with is B) and after running a quick test it turned out to be valid as well...

I rewrote the same code in Java with the exact same results...

As far as I know this is a limitation of Java/Kotlin generics. I would like to know how I can check this cast. Is it even possible?

Code

Here is a minimal working (or not working) example:

abstract class A
class B : A()
class C : A()

abstract class Util<T : A> {
    abstract fun getName(): String
    companion object {
        fun <T : A> getUtil(t: T): Util<T> = when(t) {
            is B -> UtilB() as Util<T> // warning
            is C -> UtilC() // this event gives an error
            else -> throw IllegalArgumentException("No util for this class.")
        }
    }
}

class UtilB : Util<B>() {
    override fun getName(): String = "B"
}

class UtilC : Util<C>() {
    override fun getName(): String = "C"
}

fun main() {
    val b = B()
    val c = C()
    val utilB = Util.getUtil(b)
    val utilC = Util.getUtil(c)
    println(utilB.getName()) // prints B
    println(utilC.getName()) // prints C
}
2

2 Answers

0
votes

Here is the modified version that works:

abstract class A
class B : A()
class C : A()

abstract class Util<T : A> {
    abstract fun getName(): String
    companion object {
        fun <T : A> getUtil(t: T): Util<A> = when(t) {    // here return Util<A>
            is B -> UtilB()                               // No need cast
            is C -> UtilC()
            else -> throw IllegalArgumentException("No util for this class.")
        }
    }
}

class UtilB : Util<A>() {                   // Replace B by A
    override fun getName(): String = "B"
}

class UtilC : Util<A>() {                  // Replace C by A
    override fun getName(): String = "C"
}

fun main() {
    val b = B()
    val c = C()
    val utilB = Util.getUtil(b)
    val utilC = Util.getUtil(c)
    println(utilB.getName()) // prints B
    println(utilC.getName()) // prints C
}

But I'm not sure you're on the right way. When I see this pattern I see sealed classes.

Here is an implementation using sealed classes:

sealed class AA {
    class BB : AA()
    class CC : AA()
}
sealed class AAU {
    abstract fun getName():String

    class BBU : AAU() {
        override fun getName()= "BB"
    }

    class CCU : AAU(){
        override fun getName()= "CC"
    }
}

fun getU(aa: AA) =
    when(aa) {
        is AA.BB -> AAU.BBU()
        is AA.CC -> AAU.CCU()
    }

fun main() {
   val bb = AA.BB()
   val cc = AA.CC()
   val bbu = getU(bb)
   val ccu = getU(cc)
   println(bbu.getName())
   println(ccu.getName())
}

So here no need to throw exception.

It could be even simpler:

sealed class AA {
    class BB : AA()
    class CC : AA()
}

fun getName(aa: AA) =
        when(aa) {
            is AA.BB -> "BB"
            is AA.CC -> "CC"
        }

fun main() {
    val bb = AA.BB()
    val cc = AA.CC()
    println(getName(bb))
    println(getName(cc))
}

As I don't know your context the last implementation it's perhaps not the right approach.

0
votes

To my understanding Kotlin smart cast should know that UtilB() as Util<T> is checked by t is B.

No it shouldn't because it's wrong. It's perfectly legal to call

getUtil<A>(B())

in which case t is B is true, but T is A and UtilB doesn't extend Util<A>. You may try to fix it with covariance, but that won't work either:

class D : B()

getUtil<D>(D())

Now UtilB has to extend Util<D> as well.

A possible solution is to add an F-bounded type parameter to A, but the method still has to be a member to avoid casts (Kotlin doesn't refine type parameters inside a when branch like e.g. Scala does):

abstract class A<T : A<T>> {
    abstract fun util(): Util<T>
}
class B : A<B>() {
    override fun util() = UtilB()
}
class C : A<C>() {
    override fun util() = UtilC()
}

abstract class Util<T : A<T>> {
    abstract fun getName(): String
}

class UtilB : Util<B>() {
    override fun getName(): String = "B"
}

class UtilC : Util<C>() {
    override fun getName(): String = "C"
}