El partido hecho de eventos
Al terminar: Reconstruir Partido a partir de sus eventos y calcular el resultado con un when exhaustivo.
Al terminar esta lección Marcador sabrá de qué está hecho un partido. Hasta ahora un Partido guardaba su marcador en dos números. Desde ahora guarda lo que pasó —goles, tarjetas, cambios— y el marcador se calcula a partir de eso. Un marcador que se deduce no puede contradecir los eventos; uno guardado sí. Temas: listas que «crecen» sin cambiarse, funciones de extensión, count y forEach (como adelanto de las lambdas), y una crónica del partido en Main.kt.
Guardar el marcador o deducirlo
Si Partido guarda golesLocal = 2 y a la vez una lista de goles con tres goles locales, ¿cuál dice la verdad? Con dos fuentes de datos, pueden contradecirse. La solución es guardar una sola: la lista de lo que pasó. El marcador deja de ser un dato y pasa a ser una cuenta: «cuántos de estos eventos son goles del Rayo». Para escribir eso hacen falta cinco piezas que todavía no has visto; las cinco, con datos ajenos a Marcador.
Una lista que no se cambia, solo se amplía
Una lista es un valor que guarda muchos valores del mismo tipo, en orden. List<String> es una lista de textos. Las listas que usas hasta ahora (listOf(...)) no se pueden modificar: no tienen «añadir». Para tener una con un elemento más, se crea otra, y el operador + lo hace: lista + elemento es una lista nueva con el elemento al final, y la original queda como estaba. emptyList() es la lista vacía, el punto de partida. Tres detalles más: size dice cuántos elementos hay, [1] pide el elemento de una posición, que se cuenta desde 0 (la posición 0 es el primero), y first() pide directamente el primer elemento (en un texto, first() da su primera letra).
fun main() {
val vacia: List<String> = emptyList() // una lista de textos sin elementos
val compra = vacia + "pan" // + crea una lista NUEVA con "pan" al final
val mas = compra + "leche" + "queso" // se puede encadenar
println(vacia) // la original no ha cambiado
println(compra)
println(mas)
println(mas.size) // size: cuántos elementos hay
println(mas[1]) // [1]: el elemento en la posición 1 (se cuenta desde 0)
println(mas.first()) // first(): el primer elemento de la lista
}[]
[pan]
[pan, leche, queso]
3
leche
panEs la misma idea que copy de data class e igualdad: en vez de modificar, se crea otro valor con el cambio. Las listas se ven a fondo en Colecciones.
Función de extensión: añadirle una función a un tipo
Una función de extensión es una función que escribes fuera de un tipo, pero que se llama como si fuera suya: "hola".gritar(). Sirve para dar funciones a tipos que ya existen, sea String o uno tuyo, sin tocar su código. Se escribe con el tipo delante del nombre, y dentro de ella this es el valor sobre el que se llama. Dentro de la función, this. se puede omitir: uppercase() a secas significa this.uppercase(), y si el tipo tiene una propiedad nombre, escribir nombre es lo mismo que this.nombre.
// Una función de extensión: se escribe fuera de String, pero se llama como si fuera suya.
// Dentro, this es el texto sobre el que se llama.
fun String.gritar(): String = this.uppercase() + "!"
// Con this implícito: this. se puede omitir. uppercase() sola es this.uppercase().
fun String.susurrar(): String = lowercase() + "..."
enum class Luz { ROJA, VERDE }
// También sirve con tus propios tipos. this es la Luz sobre la que se llama.
fun Luz.texto(): String = when (this) {
Luz.ROJA -> "Para"
Luz.VERDE -> "Pasa"
}
fun main() {
println("hola".gritar()) // se lee como si String tuviera esa función
println("gol de Ana".gritar())
println("SILENCIO".susurrar()) // dentro, lowercase() es this.lowercase()
println(Luz.VERDE.texto())
}HOLA!
GOL DE ANA!
silencio...
PasaVerás las extensiones a fondo en Extensiones; por ahora, léelas como una función normal que recibe el valor y se escribe con un punto.
when sin argumento y propiedades con get()
Hasta ahora el when miraba un valor (when (luz) { … }). Sin nada entre paréntesis, cada rama es una condición y gana la primera que da true; el else final recoge todo lo demás. Y una propiedad calculada se escribe con get() (la «lectura»): es la función que se ejecuta cada vez que alguien lee la propiedad, y lo que va después del = es lo que devuelve. Si necesita varias líneas, lleva llaves y un return.
// when sin argumento: cada rama es una condición; gana la primera que se cumple
fun tarifa(edad: Int): String = when {
edad < 12 -> "Infantil"
edad < 65 -> "General"
else -> "Jubilado" // si ninguna condición se cumplió
}
class Cuenta(val saldo: Int) {
// get() = …: se calcula en cada lectura, no se guarda
val enRojo: Boolean get() = saldo < 0
// get() con llaves: varias líneas y un return
val resumen: String
get() {
return if (enRojo) "Debes $saldo" else "Tienes $saldo"
}
}
fun main() {
println(tarifa(8))
println(tarifa(30))
println(tarifa(70))
println(Cuenta(-5).enRojo)
println(Cuenta(20).resumen)
}Infantil
General
Jubilado
true
Tienes 20count y forEach: un bloque que se ejecuta por cada elemento
Las listas traen funciones que reciben un bloque de código entre llaves y lo ejecutan para cada elemento. Es una lambda, y se ve a fondo en Lambdas y funciones de orden superior. Lo único que necesitas hoy:
count { … }cuenta los elementos para los que el bloque datrue(«sí»).forEach { … }ejecuta el bloque una vez por cada elemento.- Dentro del bloque,
ites el elemento de esa vuelta. &&significa «y»: la condición es verdadera solo si las dos lo son (lo viste en Condiciones: if y when). Y después deit is Cafe, Kotlin ya trataitcomo unCafe(el smart cast de la lección anterior) y deja leer sus propiedades.
Todo junto, con una cafetería y una cuenta que se amplía pedido a pedido:
// Las líneas de la cuenta: una interfaz sellada, como el EventoDePartido que ya conoces
sealed interface Linea { val precio: Int }
data class Cafe(override val precio: Int, val grande: Boolean) : Linea
data class Tostada(override val precio: Int) : Linea
data class Cuenta(
val mesa: Int,
val lineas: List<Linea> = emptyList(), // List<Linea>: lista de líneas; vacía por defecto
) {
// Propiedad calculada: no se guarda, se calcula cada vez que se lee.
// count { … } cuenta las líneas que son un Cafe Y además grande.
val grandes: Int get() = lineas.count { it is Cafe && it.grande }
// lineas + linea crea una lista NUEVA con la línea al final; la original no cambia.
// copy(lineas = …) devuelve una Cuenta nueva con esa lista.
fun pedir(linea: Linea): Cuenta = copy(lineas = lineas + linea)
}
fun main() {
val vacia = Cuenta(mesa = 3)
val llena = vacia // la cadena se puede partir antes de cada punto:
.pedir(Cafe(2, grande = true)) // cada .pedir(…) actúa sobre la Cuenta que devolvió la anterior
.pedir(Tostada(3))
.pedir(Cafe(1, grande = false))
llena.lineas.forEach { println(it) } // forEach: el bloque se ejecuta una vez por elemento
println("Antes ${vacia.lineas.size}, después ${llena.lineas.size}; grandes: ${llena.grandes}")
}Al ejecutarlo, imprime:
Cafe(precio=2, grande=true)
Tostada(precio=3)
Cafe(precio=1, grande=false)
Antes 0, después 3; grandes: 1Fíjate en que vacia sigue vacía: cada .pedir(...) devuelve una cuenta nueva y la original no se toca.
Reto · Ejercicio: el partido de baloncesto
Fuera de tu proyecto, en el Kotlin Playground (https://play.kotlinlang.org).
Una jugada de baloncesto es una canasta (de un equipo, con sus puntos) o una falta (de un equipo). Nombres exactos: una sealed interface Jugada con la propiedad equipo (un texto), y dos data class, Canasta (con equipo y puntos) y Falta (con equipo). Un data class Partido guarda la lista jugadas, vacía al empezar, y no cambia nunca: registrar(jugada) devuelve otro partido con ella al final, y faltasDe(equipo) cuenta las faltas de ese equipo. Una función describir(jugada) da una frase por jugada. Registra una canasta de 2 de Lobos, una falta de Osos, una canasta de 3 de Osos y otra falta de Osos; describe cada jugada y muestra cuántas jugadas había antes y después de registrarlas, y las faltas de Osos. La consola debe mostrar:
Canasta de 2 para Lobos
Falta de Osos
Canasta de 3 para Osos
Falta de Osos
Jugadas: antes 0, después 4
Faltas de Osos: 2Ver solución
sealed interface Jugada {
val equipo: String
}
data class Canasta(override val equipo: String, val puntos: Int) : Jugada
data class Falta(override val equipo: String) : Jugada
fun describir(jugada: Jugada): String = when (jugada) {
is Canasta -> "Canasta de ${jugada.puntos} para ${jugada.equipo}"
is Falta -> "Falta de ${jugada.equipo}"
}
data class Partido(val jugadas: List<Jugada> = emptyList()) {
fun registrar(jugada: Jugada): Partido = copy(jugadas = jugadas + jugada)
fun faltasDe(equipo: String): Int = jugadas.count { it is Falta && it.equipo == equipo }
}
fun main() {
val inicio = Partido()
val alFinal = inicio
.registrar(Canasta("Lobos", 2))
.registrar(Falta("Osos"))
.registrar(Canasta("Osos", 3))
.registrar(Falta("Osos"))
alFinal.jugadas.forEach { println(describir(it)) }
println("Jugadas: antes ${inicio.jugadas.size}, después ${alFinal.jugadas.size}")
println("Faltas de Osos: ${alFinal.faltasDe("Osos")}")
}Al ejecutarlo imprime las seis líneas de arriba. Fíjate en que inicio sigue teniendo cero jugadas: registrar no modifica el partido, devuelve otro.
Paso · El partido hecho de eventos
Objetivo. Hacer que Partido deduzca el marcador de sus eventos, añadir titular() a Resultado.kt y escribir en Main.kt la crónica de un partido. Al terminar, ./gradlew :torneo:run imprime la crónica y el resultado, y ./gradlew :torneo:test pasa con 30 tests.
Lo que se borra: el marcador guardado
En data class e igualdad Partido guardaba golesLocal y golesVisitante y tenía conGolLocal() y conGolVisitante(). Todo eso se borra: ahora el marcador sale de los eventos. También se borra la desestructuración de cuatro campos de Main.kt, porque Partido pasa a tener tres (local, visitante, eventos). Los tests de Partido que ya tenías usaban esas funciones, así que, hasta que los sustituyas al final de este paso, ./gradlew :torneo:test no compilará: es lo esperado.
En Main.kt, borra todo lo que hay desde la línea del comentario // data class: copy, == frente a ===, desestructuración. (incluida) hasta la última } de main (también incluida): son las líneas de la lección de data class y las tres líneas provisionales de la lección anterior. En su lugar pegarás el bloque nuevo, que ya trae esa } al final. El resto del fichero no cambia.
Piezas nuevas de este paso, todas vistas arriba: List<…> con emptyList(), lista + elemento, count { … } con &&, forEach { … }, una propiedad calculada con get(), un when sin argumento (el de resultado()), una función de extensión con when (this) (y this. omitido), lista[1] y first(), y una cadena de llamadas partida en varias líneas.
Intenta escribirlo tú, en este orden:
- El resultado. En
Resultado.kt, una función de extensióntitular()sobreResultadoque devuelva una frase: «Gana Rayo FC» (con el nombre del ganador) o «Empate». - El nuevo
Partido. Ya no guarda los goles. Nombres exactos, porque los tests copiados los usan: guardaeventos(la lista deEventoDePartido, vacía por defecto); una funcióngolesDe(equipo)que cuenta los goles de ese equipo; las propiedades calculadasgolesLocalygolesVisitante, que usangolesDe;registrar(evento), que devuelve un partido nuevo con el evento al final; yresultado(), que devuelve unResultado(Victoria(ganador, perdedor)oEmpate). - La crónica en
main. Juega un Rayo FC-Toros con estos cinco eventos: gol de Ana (Rayo FC) en el 12, tarjeta amarilla a Iván en el 30, gol de Iván (Toros) en el 55, gol de Ana en el 80 y cambio en el 85 (sale Ana, entra Luis). Cuenta los eventos antes y después, describe cada uno y termina con el marcador y el titular.
La línea Antes: 0-0, después: 2-1 de la lección de data class e igualdad desaparece (el Partido ya no cuenta goles así). Tras Con otro dorsal: …, la salida debe acabar así:
Antes: 0 eventos, después: 5
12' Gol de Ana (Rayo FC)
30' Tarjeta amarilla para Iván
55' Gol de Iván (Toros)
80' Gol de Ana (Rayo FC)
85' Cambio: sale Ana, entra Luis
Marcador: 2-1 -> Gana Rayo FCCuando termines, abre el bloque y compara con tu código. En titular(), ganador es this.ganador: tras is Resultado.Victoria, Kotlin ya sabe que this es una Victoria, y el this. se omite. Los bloques con ruta llevan la del fichero en tu proyecto.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
package com.sebastiannarvaez.marcador.torneo
// Un partido acaba en victoria (de alguien) o en empate.
sealed interface Resultado {
// Lleva datos: quién ganó y quién perdió.
data class Victoria(val ganador: Equipo, val perdedor: Equipo) : Resultado
// No lleva datos: basta un único objeto. `data object` da un toString legible.
data object Empate : Resultado
}
fun Resultado.titular(): String = when (this) {
is Resultado.Victoria -> "Gana ${ganador.nombre}"
Resultado.Empate -> "Empate"
}package com.sebastiannarvaez.marcador.torneo
// Sigue siendo inmutable. El marcador ya no se guarda: se DEDUCE de los
// eventos, así que no puede contradecirlos.
data class Partido(
val local: Equipo,
val visitante: Equipo,
val eventos: List<EventoDePartido> = emptyList(),
) {
fun golesDe(equipo: Equipo): Int = eventos.count { it is Gol && it.equipo == equipo }
val golesLocal: Int get() = golesDe(local)
val golesVisitante: Int get() = golesDe(visitante)
// Registrar un evento devuelve un partido nuevo con el evento al final.
fun registrar(evento: EventoDePartido): Partido = copy(eventos = eventos + evento)
fun resultado(): Resultado = when {
golesLocal > golesVisitante -> Resultado.Victoria(ganador = local, perdedor = visitante)
golesLocal < golesVisitante -> Resultado.Victoria(ganador = visitante, perdedor = local)
else -> Resultado.Empate
}
}Este es el bloque final de Main.kt: pégalo en lugar de lo que borraste, hasta la última } de main (ya la incluye):
// data class: copy y == frente a ===.
val toros = Equipo("Toros", listOf(Jugador("Iván", 7)))
val ana = jugadores.first()
val ivan = toros.plantilla.first()
println("¿Misma ficha? ${ana == Jugador("Ana", 9)} (==), ${ana === Jugador("Ana", 9)} (===)")
println("Con otro dorsal: ${ana.copy(dorsal = 10)}")
// sealed interface y when exhaustivo: los eventos de un partido.
val inicio = Partido(rayo, toros)
val alFinal = inicio
.registrar(Gol(12, ana, rayo))
.registrar(Tarjeta(30, ivan, ColorDeTarjeta.AMARILLA))
.registrar(Gol(55, ivan, toros))
.registrar(Gol(80, ana, rayo))
.registrar(Cambio(85, sale = ana, entra = jugadores[1]))
println("Antes: ${inicio.eventos.size} eventos, después: ${alFinal.eventos.size}")
alFinal.eventos.forEach { println(describir(it)) }
println("Marcador: ${alFinal.golesLocal}-${alFinal.golesVisitante} -> ${alFinal.resultado().titular()}")
}Ejecuta ./gradlew :torneo:run en la Terminal de Android Studio (View → Tool Windows → Terminal, que se abre en la carpeta del proyecto, donde está gradlew). Las últimas líneas de la salida son las del punto de control de abajo.
Los tests
Los tests de este paso son de relleno: comprueban describir y resultado() sobre el código que acabas de escribir, con las comprobaciones que ya conoces (@Test y assertEquals). EventoTest.kt es nuevo y PartidoTest.kt ya lo tienes, así que el nuevo lo sustituye entero: es el mismo test adaptado a los eventos.
EventoTest.kt (nuevo). Primero crea el fichero: en torneo → kotlin+java, clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (test) (el que lleva (test) detrás) → New → Kotlin Class/File → File → EventoTest → Intro. Después pulsa Copiar fichero, que copia el contenido al portapapeles. En el editor, selecciona todo (Cmd+A) y pega (Cmd+V): el fichero copiado trae su propia línea package, y sustituye a la que escribió Android Studio.
PartidoTest.kt (el de antes, sustituido). Abre PartidoTest.kt en el paquete (test) con doble clic y pulsa Copiar fichero. Después selecciona todo en el editor (Cmd+A) y pega (Cmd+V): el contenido nuevo sustituye al anterior.
Ejecuta ./gradlew :torneo:test. Ahora sí debe compilar y terminar en BUILD SUCCESSFUL.
Punto de control
./gradlew :torneo:runtermina enBUILD SUCCESSFULy las últimas líneas de la salida son:
Antes: 0 eventos, después: 5
12' Gol de Ana (Rayo FC)
30' Tarjeta amarilla para Iván
55' Gol de Iván (Toros)
80' Gol de Ana (Rayo FC)
85' Cambio: sale Ana, entra Luis
Marcador: 2-1 -> Gana Rayo FC./gradlew :torneo:testpasa. Abre el informe (open torneo/build/reports/tests/test/index.html, como en Bucles y tu primer test) y comprueba que pone 30 tests en total: 5 dePuntosTest, 6 deJugadorTest, 7 deEquipoTest, 5 deEventoTesty 7 dePartidoTest.- En
EventoDePartido.ktelwhendedescribirno tieneelse. Prueba a quitar la ramais Cambio ->: Android Studio y Gradle dicen'when' expression must be exhaustive. Devuélvela. Partidoya no tieneconGolLocalniconGolVisitante, ygolesLocalygolesVisitantese calculan (no están en el constructor).
Comprueba lo que entendiste
6 preguntas · necesitas 5 para completar la lección
1.En Marcador,
Partidodeja de guardargolesLocalygolesVisitantey los calcula a partir deeventos. ¿Qué se gana?2.Tienes
val a = Partido(rayo, toros)y hacesval b = a.registrar(Gol(10, ana, rayo)). ¿Cuántos eventos tieneadespués?3.Con
val letras = listOf("a", "b", "c"), ¿qué esletras[1]?4.
fun Resultado.titular(): String = when (this) { … }está escrita fuera deResultado, y aun así se llamaresultado.titular(). ¿Cómo se llama eso y qué esthisdentro?5.En
eventos.count { it is Gol && it.equipo == equipo }, ¿qué cuentacount?6.Dentro de
fun String.gritar(): String = uppercase() + "!"no aparecethis. ¿Qué pasa al llamar"hola".gritar()?