Genéricos En Kotlin

En este tutorial verás los fundamentos básicos para declarar y usar genéricos en Kotlin, con el fin de crear clases, interfaces y funciones que ejecuten código sin conocer el tipo del parámetro usado.

Parámetros Genéricos

Escribir clases, interfaces y funciones genéricas (generics) se basa en la introducción de parámetros en la sintaxis de sus declaraciones.

La lista de parámetros para tipos se incluyen en paréntesis angulares y se separan por coma si son varios.

<T, U, V,..>

Esto permite que los tipos sean pasados como argumentos a la declaración del tipo genérico, el cual los usará en sus interior.

// Función de nivel superior genérica
fun <T> funcionGenerica(param: T): T {
    return param
}

// Clase genérica
class ClaseGenerica<T>(t: T) {
    val propiedad: T = t
    fun metodo(param: T) {
        println(param)
    }
}

// Interfaz genérica
interface InterfazGenerica<T> {
    fun add(item: T)
}

Por ejemplo, cuando usas la interfaz List<E>, E es el parámetro de tipo formal y se podría leer como “una lista de E”. Lo que te permite crear listas parametrizadas según tu necesidad:

val numbers: List<Int> = listOf(1,2,3)

Al igual que los tipos básicos, el tipo parametrizado se puede inferir por el compilador si el contexto o contenido de inicialización es explícita. La declaración anterior se puede escribir así:

val numbers = listOf(1,2,3)

Declarar Funciones Genéricas

Para crear una función genérica debes añadir el tipo parámetro luego de la palabra fun.

El parámetro de tipo puede ser usado en:

  • La lista de parámetros regulares de la función
  • El valor de retorno
  • En el tipo recibidor de una función de extensión
  • En tipos función con recibidor

Por ejemplo:

fun <T> T.formatToPrint(): String = "Contenido: $this"

La anterior función de extensión formatToPrint() es una función parametrizada por T como recibidor, donde cada objeto que sirva de argumento del parámetro, será usado para crear un String con el formato establecido.

fun main() {

    println("Ejemplo".formatToPrint())
    println(6.formatToPrint())
    println(6.4.formatToPrint())
    println((6 > 5).formatToPrint())
}

De esta forma es posible invocar la función sobre múltiples tipos, donde el resultado será el formato de contenido:

Contenido: Ejemplo
Contenido: 6
Contenido: 6.4
Contenido: true

Declarar Clases Genéricas

Declara clases e interfaces genéricas anteponiendo los paréntesis angulares con los parámetros de tipos antes del nombre de la clase. Con esto, los argumentos podrán usarse en el cuerpo del tipo genérico para declarar miembros.

class Generica<T>(/*constructor primario*/){ /*cuerpo*/}

Por ejemplo:

Supongamos que deseamos diseñar el guardado de ítems de un jugador en un alijo con una capacidad máxima de compartimientos. Los ítems que pueden almacenarse están definidos por la siguiente interfaz:

interface StorableItem {
    val id: String
    val consumeSpace: Int
}

Los ítems almacenables se ven como las siguientes clases para cascos y botas:

data class Helmet(
    override val id: String,
    override val consumeSpace: Int = 1
) : StorableItem

data class Boots(
    override val id: String,
    override val consumeSpace: Int = 2
) : StorableItem

Para representar el alijo que contiene una lista de estos elementos usamos los paréntesis angulares con el parámetro de tipo formal T y especificamos una restricción para determinar que debe implementar a StorableItem.

class Stash<T : StorableItem>(var capacity: Int = 10) {

    private val itemsInStash = mutableListOf<T>()

    fun storeItem(item: T) {
        if (capacity - item.consumeSpace >= 0) {
            capacity -= item.consumeSpace
            itemsInStash.add(item)
        }
    }

    fun showItems() = itemsInStash
        .joinToString(separator = "\n") { it.id }
}

Con este simple diseño Stash te permite guardar ítems a través del método genérico storeItem() y es posible mostrar los ítems actuales del tipo parametrizado para itemsInStash con showItems().

fun main() {
    val helmetStash = Stash<Helmet>().apply {
        storeItem(Helmet("Casco de madera"))
        storeItem(Helmet("Casco de cuero"))
    }
    val bootsStash = Stash<Boots>().apply {
        storeItem(Boots("Botas maltrechas"))
        storeItem(Boots("Botas pesadas"))
    }

    println("Cascos:\n${helmetStash.showItems()}")
    println("Botas: \n${bootsStash.showItems()}")
}

Salida:

Cascos:
Casco de madera
Casco de cuero
Botas: 
Botas maltrechas
Botas pesadas

Como ves, se creó un alijo para cascos y otro para botas a través de la función apply{} y luego se imprimió el resultado a partir de Stash.showItems().

Incluso si deseas mezclar los ítems en un solo alijo, puedes declarar el tipo parametrizado Stash<StorableItem>. Así serán mezclados los elementos usando las características que StorableItem provee.

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!