Home > Software design >  How do I get all properties from an arbitrary Kotlin object?
How do I get all properties from an arbitrary Kotlin object?

Time:02-02

This is another Kotlin oddity I've run into.

I had this code:

TableCharsets::class.declaredMemberProperties.asSequence()
    .map { p -> p.get(TableCharsets) }

and it worked fine.

Then I wanted to do it for more than one object in a loop. So I thought I could write this:

sequenceOf(TableCharsets, Iso2022Charsets, EucCharsets, ShiftJisCharsets).forEach { obj ->
    obj::class.declaredMemberProperties.asSequence()
        .map { p -> p.get(obj) }
}

But the compiler complains about the call to p.get(obj).

And indeed, if I write this:

val obj = TableCharsets
obj::class.declaredMemberProperties.asSequence()
    .map { p -> p.get(obj) }

This gives the same error. Apparently p.get(R) takes Nothing, so there is no possible object I can pass in which would be acceptable.

Thinking that maybe I lost the type of the object somehow, I tried extracting to a function so that it had a known but generic type:

fun <T: Any> extract(obj: T): Sequence<Any> {
    obj::class.declaredMemberProperties.asSequence()
        .map { p -> p.get(obj) }
}

Again, I get the error that p.get(R) only takes Nothing, and won't let me pass in a T.

When I hover over declaredMemberProperties, IDEA says that it returns a Collection<KProperty<T, *>>, but somehow the property p inside my lambda is a KProperty1<out Any, *>, so that's surely the problem. But it makes no sense to me right now how it is getting that type.

How can I make this work?

CodePudding user response:

When we use obj::class then it is unknown at the compile time, what exactly is the type of obj. We only know its upper bounds, but we don't know the specific type. Therefore, we don't know who is the owner of acquired members and what object do we have to use to access them. For this reason by default obj::class returns KClass<out MyType> which means we can't pass any owner object to it.

Unfortunately, the compiler is not smart enough to recognize that you acquired KClass from exactly the same object that you then use to access members. I believe in that case the operation is safe to do and you can force the compiler to allow it by making an unchecked cast:

(obj::class as KClass<T>).declaredMemberProperties

CodePudding user response:

The difference is right at the start actually. TableCharsets::class is of type KClass<TableCharsets> - it's exactly that class. However, it's different when the object instance is extracted to a variable like this:

val obj = TableCharsets
val kClass = obj::class

Here obj is an instance of the class TableCharsets, and its static type is TableCharsets. However when calling ::class on it, the compiler can only be sure that it's TableCharsets OR a subclass of TableCharsets, so the class instance you get is of type KClass<out TableCharsets>.

This means that when iterating the properties, you cannot know that obj will be sufficient to satisfy the argument of get. Effectively, you're trying to pass an out-only type in an in position (the argument of get).

You can see the problem more clearly here:

val obj: Parent = Child()
val kClass: KClass<out Parent> = obj::class // actually KClass<Child>
kClass.declaredMemberProperties.asSequence()
    .map { p -> p.get(Parent()) } // shouldn't accept a Parent instance here

Parent() is obviously not acceptable as input, and from the compiler's point of view obj is also just of Parent type, and thus not acceptable as input, even though it's the object you initially got the KClass from (the compiler doesn't realize that)

  •  Tags:  
  • Related