Home > OS >  How can I circumvent Kotlin's generics type variance constraints
How can I circumvent Kotlin's generics type variance constraints

Time:10-17

I am relatively new Kotlin and Generics kind of give me a headache. I have the following architecture made out of:

  • A few data classes
  • A generic interface to process data
  • Implementations of that processing interface for each data type
  • A generic processing job class containing the data to be processed and it's appropriate processor
  • A global (singleton) processor which implements the processing interface, takes processing jobs and just delegates the processing to the job processor. It doesn't care about the data itself at all.

The simplified code looks like this

class DataOne
class DataTwo

interface DataProcessor<in T> {
    fun process(o: T)
}

class DataOneProcessor: DataProcessor<DataOne> {
    override fun process(o: DataOne) = println("Processing DataOne")
}

class DataTwoProcessor: DataProcessor<DataTwo> {
    override fun process(o: DataTwo) = println("Processing DataTwo")
}

class ProcessingJob<T>(val data: T, val processor: DataProcessor<T>)

object GlobalProcessor: DataProcessor<ProcessingJob<Any>> {

    override fun process(job: ProcessingJob<Any>) = job.processor.process(job.data)

}

fun main() {
    GlobalProcessor.process(ProcessingJob(DataOne(), DataOneProcessor()))
}

In the main function I get a compiler error

Type mismatch.
Required: ProcessingJob<Any>
Found: ProcessingJob<DataOne>

I understand why this happens: A DataProcessor of DataOne, viewed as a DataProcessor of Any could be asked to process DataTwos and for type safety this is not allowed.

Can you give me any suggestions on how/what to change to make it compile and achieve the required result? Thanks for your time!

CodePudding user response:

There are two problems here.

First, Any isn't actually the top-level type. Any implies not null, but T is unconstrained, which means it can be a nullable type. In this case you can use *, or you could also specify the type as Any?.

Change the signature of the GlobalProcessor to this:

object GlobalProcessor: DataProcessor<ProcessingJob<*>> {
    override fun process(job: ProcessingJob<*>): ...

The second problem is that the implementation of process can't take advantage of the generic information from the job in order to know that the job.processor and the job.data are compatible. It just sees two objects of unknown type. To let it know they share a compatible type, you need to capture that type as a type variable. We can't add a generic type parameter to the existing method, because it has to match the signature of the interface method, but we can add a new private method that introduces the generic parameter.

Here's the GlobalProcessor with both the required changes.

object GlobalProcessor: DataProcessor<ProcessingJob<*>> {
    override fun process(job: ProcessingJob<*>) = processGeneric(job)
    private fun <T> processGeneric(job: ProcessingJob<T>) = job.processor.process(job.data)
}
  • Related