Create generic 2D array in Kotlin

2019-02-12 13:58发布

问题:

Suppose I have a generic class and I need a 2D array of generic type T. If I try the following

class Matrix<T>(width: Int, height: Int) {
    val data: Array<Array<T>> = Array(width, arrayOfNulls<T>(height))
}

the compiler will throw an error saying "Cannot use 'T' as reified type parameter. Use a class instead.".

回答1:

Just because the syntax has moved on a bit, here's my take on it:

class Array2D<T> (val xSize: Int, val ySize: Int, val array: Array<Array<T>>) {

    companion object {

        inline operator fun <reified T> invoke() = Array2D(0, 0, Array(0, { emptyArray<T>() }))

        inline operator fun <reified T> invoke(xWidth: Int, yWidth: Int) =
            Array2D(xWidth, yWidth, Array(xWidth, { arrayOfNulls<T>(yWidth) }))

        inline operator fun <reified T> invoke(xWidth: Int, yWidth: Int, operator: (Int, Int) -> (T)): Array2D<T> {
            val array = Array(xWidth, {
                val x = it
                Array(yWidth, {operator(x, it)})})
            return Array2D(xWidth, yWidth, array)
        }
    }

    operator fun get(x: Int, y: Int): T {
        return array[x][y]
    }

    operator fun set(x: Int, y: Int, t: T) {
        array[x][y] = t
    }

    inline fun forEach(operation: (T) -> Unit) {
        array.forEach { it.forEach { operation.invoke(it) } }
    }

    inline fun forEachIndexed(operation: (x: Int, y: Int, T) -> Unit) {
        array.forEachIndexed { x, p -> p.forEachIndexed { y, t -> operation.invoke(x, y, t) } }
    }
}

This also allows you to create 2d arrays in a similar manner to 1d arrays, e.g. something like

val array2D = Array2D<String>(5, 5) { x, y -> "$x $y" }

and access/set contents with the indexing operator:

val xy = array2D[1, 2]


回答2:

The problem is calling arrayOfNulls<T>(height) with the non-reified type parameter T. But we also can't make T reified, the compiler will throw the following error: "Only type parameters of inline functions can be reified"

So that's what we're going to do. Instead of the constructor we use an inlined factory method:

class Matrix<T> private(width: Int, height: Int, arrayFactory: (Int) -> Array<T>) {

    class object {
        inline fun <reified T>invoke(width: Int, height: Int)
                = Matrix(width, height, { size -> arrayOfNulls<T>(size) })
    }

    val data: Array<Array<T>> = Array(width, { size -> arrayFactory(size) })
}

Notice, the constructor is now private, so calling Matrix() will correctly call the new invoke() method (related question). Because the method is inlined, we can use reified generics which makes it possible to call arrayOfNulls<T>.