Cilenco
Cilenco

Reputation: 7117

Generic inline function

Let's say I have an object which helps me to deserialize other objects from storage:

val books: MutableList<Book> = deserializer.getBookList()
val persons: MutableList<Person> = deserializer.getPersonList()

The methods getBookList and getPersonList are extension functions I have written. Their logic is allmost the same so I thought I may can combine them into one method. My problem is the generic return type. The methods look like this:

fun DataInput.getBookList(): MutableList<Book> {
    val list = mutableListOf<Book>()
    val size = this.readInt()

    for(i in 0 .. size) {
        val item = Book()
        item.readExternal(this)
        list.add(item)
    }

    return list
}

Is there some Kotlin magic (maybe with inline functions) which I can use to detect the List type and generify this methods? I think the problem would be val item = T() which will not work for generic types, right? Or is this possible with inline functions?

Upvotes: 0

Views: 1081

Answers (2)

Zoe - Save the data dump
Zoe - Save the data dump

Reputation: 28238

Note:

As marstran mentioned in a comment, this requires the class to have a zero-arg constructor to work, or it will throw an exception at runtime. The compiler will not warn you if the constructor doesn't exist, so if you pick this way, make sure you actually pass a class with a zero-arg constructor.


You can't initialize generic types, in Kotlin or Java. At least not in the "traditional" way. You can't do this:

val item = T()

In Java, you'd pass a Class<T> and get the constructor. Very basic example of that:

public <T> void x(Class<T> cls){
    cls.getConstructor().newInstance(); // Obviously you'd do something with the return value, but this is just a dummy example
}

You could do the same in Kotlin, but Kotlin has a reified keyword that makes it slightly easier. This requires an inline function, which means you'd change your function to:

inline fun <reified T> DataInput.getBookList(): MutableList<T> { // Notice the `<reified T>`
    val list = mutableListOf<T>() // Use T here
    val size = this.readInt()

    for(i in 0 .. size) {
        // This is where the initialization happens; you get the constructor, and create a new instance.
        // Also works with arguments, if you have any, but you used an empty one so I assume yours is empty
        val item = T::class.java.getConstructor().newInstance()!!
        item.readExternal(this) // However, this is tricky. See my notes below this code block
        list.add(item)
    }

    return list
}

However, readExternal isn't present in Any, which will present problems. The only exception is if you have an extension function for either Any or a generic type with that name and input.

If it's specific to some classes, then you can't do it like this, unless you have a shared parent. For an instance:

class Book(){
    fun readExternal(input: DataInput) { /*Foo bar */}
}

class Person(){
    fun readExternal(input: DataInput) { /*Foo bar */}
}

Would not work. There's no shared parent except Any, and Any doesn't have readExternal. The method is manually defined in each of them.

You could create a shared parent, as an interface or abstract class (assuming there isn't one already), and use <reified T : TheSharedParent>, and you would have access to it.

You could of course use reflection, but it's slightly harder, and adds some exceptions you need to handle. I don't recommend doing this; I'd personally use a superclass.

inline fun <reified T> DataInput.getBookList(): MutableList<T> {
    val list = mutableListOf<T>()
    val size = this.readInt()
    val method = try {
        T::class.java.getMethod("readExternal", DataInput::class.java)
    }catch(e: NoSuchMethodException){
        throw RuntimeException()
    }catch(e: SecurityException){
        throw RuntimeException()// This could be done better; but error handling is up to you, so I'm just making a basic example
        // The catch clauses are pretty self-explanatory; if something happens when trying to get the method itself,
        // These two catch them
    }
    for(i in 0 .. size) {
        val item: T = T::class.java.getConstructor().newInstance()!!
        method.invoke(item, this)
        list.add(item)
    }

    return list
}

Upvotes: 1

marstran
marstran

Reputation: 28036

You cannot call the constructor of a generic type, because the compiler can't guarantee that it has a constructor (the type could be from an interface). What you can do to get around this though, is to pass a "creator"-function as a parameter to your function. Like this:

fun <T> DataInput.getList(createT: () -> T): MutableList<T> {
    val list = mutableListOf<T>()
    val size = this.readInt()

    for(i in 0 .. size) {
        val item = createT()
        /* Unless readExternal is an extension on Any, this function 
         * either needs to be passed as a parameter as well,
         * or you need add an upper bound to your type parameter
         * with <T : SomeInterfaceWithReadExternal>
         */
        item.readExternal(this)
        list.add(item)
    }

    return list
}

Now you can call the function like this:

val books: MutableList<Book> = deserializer.getList(::Book)
val persons: MutableList<Person> = deserializer.getList(::Person)

Upvotes: 3

Related Questions