fonix232
fonix232

Reputation: 2195

Kotlin generic pass-through

I'm looking for a way in Kotlin to pass generics around - but they would have fixed types by the time of compile.

My goal would be to create a base class Field<T>, which would have a public property of type T, which is backed by a ByteArray field.

The ByteArray would be converted on the run to and from T, using a global converter repository. However I can't wrap my head around Kotlin's type erasure, something I haven't met in C# (pretty much the same code would work wonderfully).

So, my altogether goal would be this class (note, this is mostly pseudocode!):

class Field<T> {

    private var actualData: ByteArray = TODO("Init here")
    public var Data: T =
        get() = getConverter().convert(actualData)
        set(value) {
            val converted = getConverter().convertBack(value)
            // Do some other nasty stuff here
            actualData = converted
        }

    private fun getConverter(): Converter<T> = TODO("This needs implementation")
}

Since the conversion steps are always the same, it would be awesome if I could dynamically grab the converter and not need to make an inheriting class every time, i.e. I could define fields that "just work":

var data1: Field<Int> = TODO("Init here")
var data2: Field<MyOjbect> = TODO("Init here")

And of course I would like to avoid the implementation of getConverter() for each type.

Is there a way to make this work in Kotlin, or should I stick to Java within this part?

(P.S.: I will obviously not name my class Field<T>, but this name seemed to be the most generic one to describe its role in this scenario).

Upvotes: 0

Views: 722

Answers (2)

Pawel
Pawel

Reputation: 17288

You can use reified type parameters, but it will never be compile-time safe and will carry runtime overhead when compared to explicitly creating delegates for each type.

If I understood your question and comment, you're looking for something like this:

Converter interface:

interface Converter<T>{
    fun convert(value: T) : ByteArray
    fun convertBack(byteArray: ByteArray) : T
}

Object (singleton) holding all converters:

import kotlin.reflect.KClass

object Converters{
    val converters = HashMap<KClass<out Any>, Converter<out Any>>()

    inline fun<reified T : Any> put(converter: Converter<T>){
        converters[T::class] = converter
    }

    fun<T: Any> get(kclass : KClass<T>) : Converter<T>{
        val converter = converters[kclass] ?: throw IllegalStateException("Missing converter for $kclass")
        return converter as Converter<T>
    }

    init {
        //add default converters?
        put(object : Converter<Int>{
            override fun convert(value: Int): ByteArray {
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
            }

            override fun convertBack(byteArray: ByteArray): Int {
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
            }
        })
    }
}

Class using converters, infline function above mimicks constructor but doesn't require explicit class argument when called:

inline fun<reified T : Any> ByteField(initialValue: T) = ByteField(initialValue, T::class)

class ByteField<T: Any>(initialValue: T, private val kclass: KClass<T>){
    private var actualData = converter.convert(initialValue)
    val converter
        get() = Converters.get(kclass)
    var data : T
        get() = converter.convertBack(actualData)
        set(value) {
            actualData = converter.convert(value)
        }
}

Usage demo (of course you can implement property delegates as well):

class Demo{
    val intField = ByteField(1)
    val stringField = ByteField("Sfield")
    val doubleField = ByteField(2.0, Double::class) // explicit constructor
}

Upvotes: 1

denis.zhdanov
denis.zhdanov

Reputation: 3744

You need delegated properties for that.

Example:

import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import kotlin.reflect.KProperty

interface Converter<T> {

    fun fromBytes(raw: ByteArray): T

    fun toBytes(typed: T): ByteArray
}

open class MyDelegate<T>(private val converter: Converter<T>) {

    private lateinit var raw: ByteArray

    operator fun getValue(thisRef: Any?, property: KProperty<*>) = converter.fromBytes(raw)

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        raw = converter.toBytes(value)
    }
}

class StringDelegate : MyDelegate<String>(object : Converter<String> {
    override fun fromBytes(raw: ByteArray) = String(raw, StandardCharsets.UTF_8)

    override fun toBytes(typed: String) = typed.toByteArray(StandardCharsets.UTF_8)
})

data class Ints(val first: Int, val second: Int)

class IntsDelegate : MyDelegate<Ints>(object : Converter<Ints> {
    override fun fromBytes(raw: ByteArray) = ByteBuffer.wrap(raw).let { Ints(it.int, it.int) }

    override fun toBytes(typed: Ints) = ByteArray(8).apply {
        with(ByteBuffer.wrap(this)) {
            putInt(typed.first)
            putInt(typed.second)
        }
    }
})

Usage:

class Test {

    var string: String by StringDelegate()

    var ints: Ints by IntsDelegate()

    override fun toString(): String {
        return "string: $string, ints: $ints"
    }
}

fun main(args: Array<String>) {
    val t = Test()

    with(t) {
        string = "first"
        ints = Ints(1, 2)
    }
    println(t) // string: first, ints: Ints(first=1, second=2)

    with(t) {
        string += " + second"
        ints = Ints(ints.first * 2, ints.second * 2)
    }
    println(t) // string: first + second, ints: Ints(first=2, second=4)
}

Please note that this all looks nice and elegant but might be rather expensive. E.g. consider ints = Ints(ints.first * 2, ints.second * 2) - here we perform ByteArray -> Ints, access the first property; make ByteArray -> Ints once agains and access the second property and finally perform Ints -> ByteArray.

In case you access properties frequently and especially when they are large and complex that might be very costly.

Upvotes: 0

Related Questions