Delegación: by lazy
Al terminar: Usar by lazy para calcular un valor solo la primera vez que se lee.
En esta lección la tabla de posiciones de una «foto» del torneo se calculará una sola vez, la primera vez que alguien la lea, gracias a by lazy. Es tu primera propiedad delegada: una propiedad cuyo valor no lo guarda ella, sino otro objeto.
Al terminar, main mostrará el contador de cálculos de la foto: 0 antes de leer la tabla y 1 después de leerla dos veces. En las tres lecciones siguientes, JSON e instalar kotlinx.serialization, Guardar y leer JSON con Json y Guardado.kt en Marcador, instalarás la serialización y guardarás el torneo en un fichero.
Delegación: quien lee la propiedad no es quien la calcula
Una propiedad delegada no guarda su valor: cuando alguien la lee (get) o la escribe (set), lo hace otro objeto, el delegado. Se escribe con by: val tabla by lazy { … }.
El problema que resuelve: hay valores que cuestan calcular (una tabla de posiciones recorre todos los partidos) y que casi siempre se leen más de una vez. Calcularlos en cada lectura es trabajo repetido, y calcularlos al crear el objeto es trabajo perdido si nadie los lee. by lazy los calcula la primera vez que se leen y guarda el resultado.
class Foto(val goles: List<Int>) {
val total: Int by lazy { // by lazy: el bloque corre en la primera lectura de total
println("calculando")
goles.sum() // la última línea es el valor que se guarda
}
}
fun main() {
val foto = Foto(listOf(1, 2, 3))
println("antes")
println(foto.total) // imprime «calculando» y luego 6
println(foto.total) // solo 6: no vuelve a calcular
}Al ejecutarlo, imprime:
antes
calculando
6
6Fíjate en las dos condiciones de by lazy: la propiedad es una val (el valor no cambia después) y el bloque se ejecuta en la primera lectura, no al crear el objeto.
Por qué el by lazy vive en InstantaneaDelTorneo y no en Torneo
Torneo es mutable: registrar añade partidos. Una tabla guardada dentro de Torneo con by lazy se calcularía una vez y no se enteraría del partido siguiente: mostraría una tabla obsoleta. Por eso el lazy va en una clase distinta, InstantaneaDelTorneo: una foto con la lista de partidos ya copiada. Como sus partidos ya no cambian, guardar la tabla es seguro.
Reto · Ejercicio: calcula una sola vez
En el Kotlin Playground, fuera de tu proyecto:
- Parte de la clase
Fotodel ejemplo mínimo. Haz una versión en la quetotalse calcule en cada lectura, como una propiedad calculada de Propiedades y visibilidad, y leetotaltres veces. - Haz lo mismo con la versión
by lazy. ¿Cuántas veces sale «calculando» en cada caso?
Pista: un get() de varias líneas se escribe con llaves, get() { … }, y devuelve el valor con return en su última línea. Para ver cuándo se calcula, haz que el cálculo imprima «calculando» antes de devolver goles.sum(). Con la lista 1, 2 y 3, y leyendo e imprimiendo total tres veces con cada versión, la salida esperada es:
calculando
6
calculando
6
calculando
6
calculando
6
6
6Ver solución
class FotoConGet(val goles: List<Int>) {
val total: Int
get() {
println("calculando")
return goles.sum()
}
}
class FotoLazy(val goles: List<Int>) {
val total: Int by lazy {
println("calculando")
goles.sum()
}
}
fun main() {
val a = FotoConGet(listOf(1, 2, 3))
println(a.total) // «calculando» y 6
println(a.total) // «calculando» otra vez
println(a.total) // «calculando» otra vez: 3 veces en total
val b = FotoLazy(listOf(1, 2, 3))
println(b.total) // «calculando» y 6
println(b.total) // solo 6: valor guardado
println(b.total) // solo 6: valor guardado
}Un get() calculado se ejecuta en cada lectura. Con by lazy se ejecuta en la primera y las demás devuelven el valor guardado. Por eso lazy compensa cuando el cálculo es caro y el resultado no cambia.
Paso · by lazy en InstantaneaDelTorneo
Objetivo. Añadir InstantaneaDelTorneo, una foto del torneo cuya tabla se calcula una sola vez, y usarla desde main. Al ejecutar ./gradlew :torneo:run aparecerán tres líneas nuevas al final, sobre la tabla calculada.
Cómo crear Instantanea.kt. En la vista Android, despliega torneo → kotlin+java, haz clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (el que no lleva (test) detrás) → New → Kotlin Class/File. En la ventanita elige File, escribe Instantanea y pulsa Intro.
Los bloques de código llevan como título la ruta del fichero en tu proyecto. En la vista Android, esos ficheros están en torneo → kotlin+java → com.sebastiannarvaez.marcador.torneo.
Piezas nuevas de este paso: by lazy con un contador que cuenta los cálculos (calculosDeLaTabla++, que suma uno), una propiedad internal var con private set (internal significa «visible dentro de este módulo»; tu módulo :torneo es la carpeta torneo del proyecto, donde viven todas las clases del torneo) y una extensión que devuelve la foto, como las de Extensiones. Todas en un ejemplo con un menú de recetas, explicado línea a línea:
class Receta(val nombre: String, val ingredientes: List<String>)
class Menu(private val recetas: List<Receta>) {
// internal: visible en todo el módulo; private set: solo esta clase la cambia
internal var preparaciones = 0
private set
// by lazy: el bloque se ejecuta en la PRIMERA lectura y el resultado se guarda
val compra: List<String> by lazy {
preparaciones++ // ++ suma uno: preparaciones = preparaciones + 1
recetas.flatMap { it.ingredientes }.toSet().toList() // toSet() quita los repetidos; toList() lo vuelve a lista
}
}
// Extensión que devuelve un objeto nuevo a partir de la receta
fun Receta.enMenu(): Menu = Menu(listOf(this))
fun main() {
val menu = Receta("Tortilla", listOf("huevo", "patata", "huevo")).enMenu()
println("Antes de leer: ${menu.preparaciones}") // 0: aún nadie ha leído compra
println(menu.compra) // primera lectura: calcula
println(menu.compra) // segunda: devuelve lo guardado
println("Después: ${menu.preparaciones}") // 1: se calculó una sola vez
}Al ejecutarlo, imprime:
Antes de leer: 0
[huevo, patata]
[huevo, patata]
Después: 1Intenta escribirlo tú. InstantaneaDelTorneo es una foto de un torneo: sus partidos, sus equipos y su reglamento. Su tabla (la tabla de posiciones de esos partidos) y sus goleadores se calculan la primera vez que alguien los lee, y nunca más. El contador calculosDeLaTabla sube en uno cada vez que la tabla se calcula de verdad; se lee desde todo el módulo, pero solo la foto lo cambia. A un torneo se le pide su foto con instantanea(). Al final de main, pide una foto, muestra el contador antes de leer la tabla, lee el líder dos veces y muestra el contador otra vez. Tras las líneas de antes, debe verse:
Tabla calculada 0 veces antes de leerla
Líder: Rayo FC
Líder otra vez: Rayo FC (calculada 1 vez)Qué se cambia y dónde (obligatorio): el bloque nuevo de Main.kt va al final de main, después de destino.delete() y antes de la llave de cierre.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Instantanea.kt, fichero nuevo entero:
package com.sebastiannarvaez.marcador.torneo
// Una «foto» del torneo en un momento dado: sus partidos ya no cambian.
// Por eso aquí SÍ tiene sentido calcular la tabla una sola vez y guardarla.
//
// `by lazy` es una propiedad delegada: en vez de guardar un valor, delega
// leer la propiedad en otro objeto (el Lazy). El bloque corre la PRIMERA vez
// que alguien lee `tabla`; las siguientes lecturas devuelven el resultado
// guardado.
class InstantaneaDelTorneo(
val partidos: List<Partido>,
val equipos: List<Equipo>,
val reglamento: Reglamento,
) {
// Solo para que los tests vean cuántas veces se calculó de verdad.
internal var calculosDeLaTabla = 0
private set
val tabla: List<FilaDePosicion> by lazy {
calculosDeLaTabla++
partidos.tablaDePosiciones(equipos, reglamento)
}
val goleadores: Ranking<Jugador> by lazy { partidos.goleadores() }
}
// `partidos` ya devuelve una copia, así que la foto no se ve afectada por lo que
// se registre después.
fun Torneo.instantanea(): InstantaneaDelTorneo = InstantaneaDelTorneo(partidos, equipos, reglamento)Main.kt, el bloque final (de momento termina aquí; la llave de cierre de main va después):
// by lazy: la tabla de una foto del torneo se calcula la primera vez que se lee.
val foto = torneo.instantanea()
println("Tabla calculada ${foto.calculosDeLaTabla} veces antes de leerla")
println("Líder: ${foto.tabla.first().equipo.nombre}")
println("Líder otra vez: ${foto.tabla.first().equipo.nombre} (calculada ${foto.calculosDeLaTabla} vez)")Abre la Terminal de Android Studio (View → Tool Windows → Terminal, que se abre en la raíz del proyecto) y ejecuta:
./gradlew :torneo:runAl final de la salida deben aparecer estas tres líneas. Fíjate en el contador: vale 0 antes de leer la tabla y 1 después de leerla dos veces.
Tabla calculada 0 veces antes de leerla
Líder: Rayo FC
Líder otra vez: Rayo FC (calculada 1 vez)Punto de control
- En la Terminal de Android Studio,
./gradlew :torneo:test :torneo:runtermina enBUILD SUCCESSFUL. Si un test falla, Gradle lo dice conFAILEDy no llega aBUILD SUCCESSFUL. - La salida termina con
Tabla calculada 0 veces antes de leerla, luegoLíder: Rayo FCyLíder otra vez: Rayo FC (calculada 1 vez). - Abre el informe de tests (
open torneo/build/reports/tests/test/index.html, como en Bucles y tu primer test): arriba pone 75 tests, los mismos que antes de esta lección, porque no hay ningún test nuevo. - En el paquete
com.sebastiannarvaez.marcador.torneoestáInstantanea.kt.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.Lees
foto.tabla(declarada conby lazy) tres veces. ¿Cuántas veces se ejecuta el bloque delazy?2.¿Por qué la tabla con
by lazyestá enInstantaneaDelTorneoy no dentro deTorneo?3.Una propiedad calculada con
get() { … }y otra conby lazy { … }hacen el mismo cálculo. Las lees tres veces. ¿Cuántas veces se calcula cada una?4.¿Por qué una propiedad con
by lazyse declara convaly no convar?