Kotlin abstract class with generic param and metho

2019-02-22 01:57发布

问题:

I'm trying to create an abstract class with generic parameter which will have subclasses which should call methods without having to specify type parameters. I have this so far:

abstract class AbstractClass<T : Any> @Autowired constructor(protected val delegate: MyService) {
    inline fun <T: Any> myMethod(param: Any): T? {
        return delegate.myMethod(param).`as`(T::class.java)
    }
}

And implementation:

class TesterWork @Autowired constructor(delegate: MyService) : AbstractClass<Tester>(delegate) {
}

Now when calling myMethod I have to specify the type argument:

testerWork.myMethod<Tester>("test")

I was wondering would it be possible to infer the type argument automatically? Can I somehow rework myMethod? Note that I need to have T::class.java inside the method.

回答1:

You cannot use a class' generic parameter as a reified generic (getting its T::class token), because at runtime the generic parameter is erased: Kotlin follows Java's type erasure practice and doesn't have reified generics for classes.
(Kotlin has reified generics only for inline functions)

Given that, it's up to you to pass and store a Class<T> token so that you can use it.

Also, myFunction in your example introduces a generic parameter, and it will be a new generic, not connected to the class generic parameter in any way (naming both T only adds confusion, consider them T1 and T2). If I get it right, you meant the class' generic instead.

Probably what you can do is declare an abstract val that would store a class token and rewrite the function so that it uses the stored class token:

abstract class AbstractClass<T : Any> constructor(protected val delegate: MyService) {
    protected abstract val classToken: Class<T>

    fun myMethod(param: Any): T? {
        return delegate.myMethod(param).`as`(classToken)
    }
}

Then, deriving from AbstractClass will require overriding the classToken:

class TesterWork constructor(delegate: MyService) : AbstractClass<Tester>(delegate) {
    override val classToken = Tester::class.java
}

After that, you will be able to call the function on TesterWork instance without specifying the generic parameter:

val service: MyService = ...
val t: Tester? = TesterWork(service).myMethod("test")


回答2:

There are a couple of problems at play here. First, in order to retrieve a class object from a generic type parameter, you need to reify it. For this you need to declare <reified T: Any>.

Second, you declare the generic type parameter twice. Once in the class declaration abstract class AbstractClass<T : Any> and once in the method declaration inline fun <T: Any> myMethod. Those Ts are not the same. Theoretically you could just leave out the one from the method signature but this doesn't work because reification only works with inline methods not with classes. For a possible solution to that refer to @hotkey's answer.



回答3:

You can give the abstract class a casting lambda:

abstract class AbstractClass<T : Any>(val delegate: MyService, val castFn: (Any) -> T) {
    fun myMethod(param: Any): T? {
        return castFn(delegate.myMethod(param))
    }
}

class TesterWork(delegate: MyService) : AbstractClass<Tester>(delegate, {it as Tester}) {

}

You could maybe also make the class non-abstract, give it an interface and let an inline function create it:

interface Inter<T> {
    fun myMethod(param: Any): T
}

class NotAbstractClass<T>(val delegate: MyService, val castFn: (Any) -> T) : Inter<T> {
    override fun myMethod(param: Any): T {
        return castFn(delegate.myMethod(param))
    }
}

inline fun <reified T> makeClass(delegate: MyService): Inter<T> {
    return NotAbstractClass<T>(delegate, { it as T })
}

Then you can delegate like this:

class TesterWork(delegate: MyService) : Inter<Tester> by makeClass(delegate)

val service: MyService = MyService()
val t: Tester? = TesterWork(service).myMethod("test")


回答4:

Thanks to Fabian Zeindly I wrote a variant for several extended classes. In this case all of them should be declared in a title of an abstract class.

abstract class AbstractAdapter<V: AbstractAdapter.ViewHolder, I: AbstractAdapter.Item> : RecyclerView.Adapter<V>() {

    var list: List<I> = emptyList()

    override fun getItemCount(): Int = list.size

    open fun setItems(list: List<I>) {
        this.list = list.toMutableList()
    }

    open class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    open class Item
}

Then extend the class with new inner classes (ViewHolder, Item).

class NewAdapter(private var listener: View.OnClickListener?) : AbstractAdapter<NewAdapter.ViewHolder, NewAdapter.Item>() {

    private var originalItems: List<Item> = emptyList()

    override fun setItems(list: List<Item>) {
        super.setItems(list)
        this.originalItems = list.toMutableList()
        this.list = list.toMutableList()
    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder {
        val view = LayoutInflater.from(viewGroup.context)
            .inflate(R.layout.layout, viewGroup, false)

        return ViewHolder(view)
    }

    override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
        val item = list[i]
        viewHolder.textView.text = item.text
        viewHolder.textView.tag = item.id
    }

    class ViewHolder(itemView: View) : AbstractAdapter.ViewHolder(itemView) {
        val textView: TextView = itemView.name
    }

    class Item(val id: Int, val text: String) : AbstractAdapter.Item()
}