Maxim
Maxim

Reputation: 9981

Incompatible types: A and kotlin.reflect.KType

I want to check that function has parameter of A type use the following code:

import kotlin.reflect.*
import javafx.event.ActionEvent

interface IA {}

class A {}

class B {
    fun test(a: A, ia: IA, event: ActionEvent) {
        println(a)
        println(ia)
        println(event)
    }
}

fun main(args: Array<String>) {
    for (function in B::class.declaredMemberFunctions) {
        for (parameter in function.parameters) {
            when (parameter.type) {
                is IA -> println("Has IA interface parameter.")
                is ActionEvent -> println("Has ActionEvent class parameter.")
                is A -> println("Has A class parameter.") // <---- compilation error in this line
            }
        }
    }
}

but when I try to compile it I see the following error:

> Error:(20, 19) Incompatible types: A and kotlin.reflect.KType

Questions:

  1. Why compiler don't raise error for IA interface and ActionEvent Java class?
  2. Why compiller raise error for A class?
  3. How to check that function has argument of A type?

Upvotes: 3

Views: 3282

Answers (2)

Jayson Minard
Jayson Minard

Reputation: 86006

First, you are using the wrong operator. is checks if an instance of something is a given class. You have no instance. You instead have a KType that you are trying to check if it is the instance of a class A, or IA or ActionEvent, it isn't.

So you need to use a different operator, which is to check if they are equal or call the method isAssignableFrom(). And then you need to check that the two things you are comparing are the right datatypes and do what you expect.

In another answer, @Michael says you can just treat a Type and Class the same for equality, that isn't always true; not that simple. Sometimes a Type is a Class but sometimes it is a ParameterizedType, GenericArrayType, TypeVariable, or WildcardType which are not comparable with equals. So that approach is wrong if you ever have a parameter to the method that uses generics it breaks.

Here is a version that does not support generics in that if generics are used in the parameter, they will not match. This also compares KTypeusing equality, which means it does not work for inherited classes matching against a superclass or interface. But the most simple is:

 when (parameter.type) {
    IA::class.defaultType -> println("Has IA interface parameter.")
    ActionEvent::class.defaultType -> println("Has ActionEvent class parameter.")
    A::class.defaultType -> println("Has A class parameter.") 
 }

This breaks if For example if class A had generic parameter T so you wanted to check a parameter that is A<String> or A<Monkey> you will not match A::class.defaultType (FALSE!!!). Or if you tried to compare array types, again will not match.

To fix this generics problem, we need to also erase the paramter.type you are checking. We need a helper function to do that.

Here is one copied from the Klutter library that takes a KType and erases the generics to make a KClass. You will need the kotlin-reflect dependency to use this code. You can remove the kotlin-refect dependency by only working with Java Class and not using KClass anywhere directly. Some other code will have to change.

With the following extension function:

fun KType.erasedType(): KClass<*> {
     return this.javaType.erasedType().kotlin
}

@Suppress("UNCHECKED_CAST")
fun Type.erasedType(): Class<*> {
    return when (this) {
        is Class<*> -> this as Class<Any>
        is ParameterizedType -> this.getRawType().erasedType()
        is GenericArrayType -> {
            // getting the array type is a bit trickier
            val elementType = this.getGenericComponentType().erasedType()
            val testArray = java.lang.reflect.Array.newInstance(elementType, 0)
            testArray.javaClass
        }
        is TypeVariable<*> -> {
            // not sure yet
            throw IllegalStateException("Not sure what to do here yet")
        }
        is WildcardType -> {
            this.getUpperBounds()[0].erasedType()
        }
        else -> throw IllegalStateException("Should not get here.")
    }
}

You can now write your code more simply as:

 when (parameter.type.erasedType()) {
    IA::class-> println("Has IA interface parameter.")
    ActionEvent::class -> println("Has ActionEvent class parameter.")
    A::class -> println("Has A class parameter.") 
 }

So generics are ignored and this works comparing the raw erased class against each other; but again without inheritance.

To support inheritance you can use this version slightly modified. You need a different form of when expression and a helper function:

fun assignCheck(ancestor: KClass<*>, checkType: KType): Boolean =
    ancestor.java.isAssignableFrom(checkType.javaType.erasedType())

Then the when expression changed to:

when {
    assignCheck(IA::class, parameter.type) -> println("Has IA interface parameter.")
    assignCheck(ActionEvent::class, parameter.type) -> println("Has ActionEvent class parameter.")
    assignCheck(A::class, parameter.type) -> println("Has A class parameter.")
}

Extra credit, comparing full generics:

To compare full generics we need to convert everything to something that we can compare that still has generics. The easiest is to get everything into a Java Type since it is harder to get everything into a KType. First we need a TypeReference type class, we'll steal this from Klutter library as well:

abstract class TypeReference<T> protected constructor() {
    val type: Type by lazy {
        javaClass.getGenericSuperclass().let { superClass ->
            if (superClass is Class<*>) {
                throw IllegalArgumentException("Internal error: TypeReference constructed without actual type information")
            }
            (superClass as ParameterizedType).getActualTypeArguments()[0]
        }
    }
}

Now a quick extension method to use this:

inline fun <reified T: Any> ft(): Type = object:TypeReference<T>(){}.type

And then our when expression can be more detailed with generics:

for (parameter in function.parameters) {
    when (parameter.type.javaType) {
        ft<IA>() -> println("Has IA interface parameter.")
        ft<ActionEvent>() -> println("Has ActionEvent class parameter.")
        ft<A<String>>()  -> println("Has A<String> class parameter.") // <---- compilation error in this line
        ft<A<Monkey>>()  -> println("Has A<Monkey> class parameter.") // <---- compilation error in this line
    }
}

But in doing this, we broke inheritance checking again. And we don't really check covariance of the generics (they themselves could have superclass checks).

Double Extra Credit, what about inheritance AND generics?!?

Um, this isn't so much fun. I'll have to think about that one a bit, maybe add it to Klutter later. It is kinda complicated.

Upvotes: 2

Michael
Michael

Reputation: 54725

The thing is you're trying to check if KType is A, which is always false. And the compiler knows it and raises a compilation error. But IA is an interface a class that implements KType can possibly implement this interface too so there's no compilation error. ActionEvent is an open class so it's subtype can implement KType - no compilation error either.

What you should do to check if the parameter type is some class or some interface is the following.

when (parameter.type.javaType) {
    IA::class.javaClass -> println("Has IA interface parameter.")
    ActionEvent::class.javaClass -> println("Has ActionEvent class parameter.")
    A::class.javaClass -> println("Has A class parameter.")
}

Upvotes: 6

Related Questions