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.
// 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
holaObserva 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.
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: 250Extensiones 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.
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=250Reto · Ejercicio: un par con dos tipos
En el Kotlin Playground, fuera de tu proyecto:
- Un par que guarde dos valores de tipos que pueden ser distintos.
- Una extensión genérica del par que devuelva otro par con los valores cambiados de lado, sin tocar la clase.
- Con el par ("Rayo", 3), imprime los dos valores del par invertido separados por un espacio.
Salida esperada:
3 RayoVer solución
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:runtermina enBUILD 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.¿Qué gana una clase genérica
Caja<T>frente a tenerCajaDeEnterosyCajaDeTextos?2.En
val c = Caja(5), ¿de dónde sale el tipoT?3.En
fun <T> primero(lista: List<T>): T, ¿para qué sirve el<T>que va antes del nombre?4.Tienes
class Par<A, B>(val primero: A, val segundo: B). ¿Qué esPar("Rayo", 3)?5.Tienes
fun <T> Caja<T>.repetida(): List<T>. ¿Con qué cajas se puede llamar?