Saltar al contenido
K
Kotlin: el lenguajePaso a paso20 min de lectura · 3 min de máquina Básico

Genéricos

Al terminar: Escribir clases y funciones que valen para varios tipos con parámetros de tipo, incluidos dos tipos a la vez y extensiones genéricas.

Cuando el mismo código sirve para tipos distintos, copiarlo una vez por tipo es una trampa: cada arreglo hay que repetirlo en todas las copias. Marcador lo va a necesitar en la lección siguiente, Comparadores, Iterable y el ranking, donde una sola clase, Ranking<T>, servirá igual para jugadores que para filas de la tabla. Esta lección te da la herramienta que lo hace posible.

El truco se llama genéricos: una clase o función que no decide el tipo con el que trabaja hasta que alguien la usa. Temas: el problema del código duplicado, clases genéricas, funciones genéricas, dos parámetros de tipo a la vez y extensiones genéricas. El proyecto no cambia en esta lección: todo se prueba en el Playground.

El problema: el mismo código para cada tipo

Imagina una caja que guarda un solo valor. Si hoy la quieres para un número y mañana para un texto, sin genéricos acabas escribiendo la misma clase dos veces, y una tercera para el siguiente tipo. Cada cambio que le hagas a una hay que repetirlo en todas.

Un genérico lo evita. La clase declara un parámetro de tipo entre ángulos, casi siempre T, que es un hueco para un tipo. Dentro de la clase, T se usa como cualquier otro tipo. Al crear un objeto, el tipo real sustituye a T: Caja<Int> guarda un entero y Caja<String>, un texto.

ejemplo mínimo: del código duplicado a una clase genérica (con comentarios)
// El problema: dos cajas casi idénticas, solo cambia el tipo de lo que guardan
class CajaDeEnteros(val contenido: Int)
class CajaDeTextos(val contenido: String)
 
// La solución: UNA sola caja. T es el parámetro de tipo: se decide al usar la clase.
class Caja<T>(val contenido: T)
 
fun main() {
    println(CajaDeEnteros(5).contenido + 1)    // la versión duplicada
    println(CajaDeTextos("a").contenido)
 
    val cajaDeEnteros = Caja(5)                // el compilador deduce Caja<Int>
    println(cajaDeEnteros.contenido + 1)       // sabe que es Int, sin conversiones
    val cajaDeTexto = Caja("hola")             // y aquí deduce Caja<String>
    println(cajaDeTexto.contenido)
}

Al ejecutarlo, imprime:

6
a
6
hola

Observa que no escribiste Caja<Int>: Kotlin lo infiere de 5. Puedes escribirlo (Caja<Int>(5)) cuando quieras dejarlo explícito.

Funciones genéricas y varios parámetros de tipo

También las funciones pueden ser genéricas: el <T> va antes del nombre de la función, y así Kotlin sabe que T es un hueco y no un tipo llamado T. Y una clase puede tener varios parámetros de tipo, uno por cada hueco, separados por comas: cada uno se decide por separado, así que pueden ser tipos distintos.

ejemplo mínimo: funciones genéricas y dos parámetros de tipo (con comentarios)
class Caja<T>(val contenido: T)
 
// Dos parámetros de tipo: N es el tipo del nombre y V el del valor
class Etiquetado<N, V>(val nombre: N, val valor: V)
 
// Función genérica: `<T>` va antes del nombre.
// first() devuelve el primer elemento de una lista (el de la posición 0).
fun <T> primero(lista: List<T>): T = lista.first()
 
fun main() {
    println(primero(listOf("a", "b")))         // T es String aquí
    println(primero(listOf(10, 20, 30)))       // y aquí T es Int: la misma función
    val peso = Etiquetado("pan", 250)          // N es String y V es Int: tipos distintos a la vez
    println("${peso.nombre}: ${peso.valor}")
}
a
10
pan: 250

Extensiones genéricas

Las dos ideas se pueden juntar: una función de extensión (las viste en Extensiones y alcance) que sirve para cualquier Caja<T>. El <T> va antes del tipo al que se añade, y es el mismo T de Caja<T>. Si la clase tiene dos parámetros de tipo, se declaran los dos, separados por coma.

ejemplo mínimo: una extensión genérica (con comentarios)
class Caja<T>(val contenido: T)
class Etiquetado<N, V>(val nombre: N, val valor: V)
 
// `<T>` antes del tipo al que se añade (el mismo `<T>` de `Caja<T>`).
// Devuelve el contenido dos veces, en una lista.
fun <T> Caja<T>.repetida(): List<T> = listOf(contenido, contenido)
 
// Con dos parámetros de tipo se declaran los dos antes del tipo: `<N, V>`.
fun <N, V> Etiquetado<N, V>.comoTexto(): String = "$nombre=$valor"
 
fun main() {
    println(Caja("hola").repetida())           // aquí T es String
    println(Caja(7).repetida())                // y aquí Int: la extensión sirve para cualquier Caja<T>
    println(Etiquetado("pan", 250).comoTexto())
}
[hola, hola]
[7, 7]
pan=250

Reto · Ejercicio: un par con dos tipos

En el Kotlin Playground, fuera de tu proyecto:

  1. Un par que guarde dos valores de tipos que pueden ser distintos.
  2. Una extensión genérica del par que devuelva otro par con los valores cambiados de lado, sin tocar la clase.
  3. Con el par ("Rayo", 3), imprime los dos valores del par invertido separados por un espacio.

Salida esperada:

3 Rayo
Ver solución
solución del reto
class Par<A, B>(val primero: A, val segundo: B)
 
fun <A, B> Par<A, B>.invertido(): Par<B, A> = Par(segundo, primero)
 
fun main() {
    val invertido = Par("Rayo", 3).invertido()
    println("${invertido.primero} ${invertido.segundo}")   // 3 Rayo
}

Una clase puede llevar varios parámetros de tipo. Y la función lleva <A, B> antes del nombre porque, si no, Kotlin buscaría dos tipos llamados A y B.

Paso · El proyecto sigue igual

Objetivo. Comprobar que Marcador sigue como lo dejaste y que los genéricos están claros antes de usarlos. No tocas ningún fichero: en la lección siguiente los genéricos se aplican a un ranking de verdad.

Punto de control

  • En la Terminal de Android Studio (View → Tool Windows → Terminal), ./gradlew :torneo:test :torneo:run termina en BUILD SUCCESSFUL.
  • Abre el informe de tests como en Bucles y tu primer test (open torneo/build/reports/tests/test/index.html) y comprueba que arriba pone 59 tests, los mismos de antes: esta lección no añade ninguno.
  • En el Playground, tu ejercicio imprime 3 Rayo.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Qué gana una clase genérica Caja<T> frente a tener CajaDeEnteros y CajaDeTextos?

  2. 2.En val c = Caja(5), ¿de dónde sale el tipo T?

  3. 3.En fun <T> primero(lista: List<T>): T, ¿para qué sirve el <T> que va antes del nombre?

  4. 4.Tienes class Par<A, B>(val primero: A, val segundo: B). ¿Qué es Par("Rayo", 3)?

  5. 5.Tienes fun <T> Caja<T>.repetida(): List<T>. ¿Con qué cajas se puede llamar?

Faltan 5 de 5