Funciones de alcance
Al terminar: Elegir entre let, run, apply, also y with para abrir un bloque sobre un objeto.
Al terminar esta lección el torneo de Marcador se creará y se rellenará de partidos en un solo gesto, y habrá una función resumenDeEquipo que construye una frase usando las cinco funciones de alcance, una por una. Todo cambia en Formato.kt y Main.kt.
Las funciones de alcance (let, run, with, apply y also) ejecutan un bloque de código «sobre» un objeto, para no repetir su nombre en cada línea. Temas: para qué sirve cada una, cuándo el objeto es this y cuándo es it, y qué devuelve cada una.
Qué problema resuelven
Cuando creas un objeto y lo configuras, el nombre del objeto se repite en cada línea:
fun main() {
// Sin función de alcance: hay que repetir el nombre del objeto en cada línea
val a = StringBuilder()
a.append("hola")
a.append(" ")
a.append("mundo")
// Con apply: dentro del bloque, this es el objeto y no hace falta repetirlo
val b = StringBuilder().apply {
append("hola")
append(" ")
append("mundo")
}
println(a)
println(b)
}hola mundo
hola mundo(StringBuilder es un objeto para ir montando un texto poco a poco, con append("…") que añade al final. Aquí solo es la pieza de ejemplo.)
Las cinco funciones hacen lo mismo en el fondo: abren un bloque sobre un objeto. Se distinguen en dos cosas: cómo llamas al objeto dentro del bloque (this o it) y qué devuelve la expresión entera (el propio objeto o lo que calcule el bloque). Y cada una tiene un uso típico:
| Función | Uso típico | Dentro del bloque el objeto es | Devuelve |
|---|---|---|---|
apply | configurar un objeto recién creado | this | el propio objeto |
also | «mirar de paso» (una traza) sin cambiar nada | it | el propio objeto |
let | hacer algo solo si no es null, o transformar un valor | it | lo que devuelve el bloque |
run | calcular algo usando las propiedades del objeto | this | lo que devuelve el bloque |
with(objeto) { } | lo mismo que run, con el objeto entre paréntesis | this | lo que devuelve el bloque |
Las que usan this: run, with y apply
Dentro del bloque, las propiedades del objeto se escriben sin prefijo: titulo en vez de libro.titulo. Es la misma idea de lambda con receptor que ya viste en buildString.
class Libro(val titulo: String, val paginas: Int)
fun main() {
val libro = Libro("Niebla", 250)
// run: dentro, this es el libro (sus propiedades sin prefijo); devuelve lo que calcula el bloque
val ficha = libro.run { "$titulo, $paginas págs." }
// with(objeto) { … }: igual que run, pero el objeto va entre paréntesis
val corto = with(libro) { paginas < 300 }
println(ficha)
println(corto)
// apply: this es el objeto y devuelve el propio objeto. Ideal para configurar
val montado = StringBuilder().apply {
append("a")
append("b")
}
println(montado)
}Niebla, 250 págs.
true
abLas que usan it: let y also
Dentro del bloque, el objeto se llama it (o el nombre que le des). let entrega lo que calculas; also deja pasar el objeto. El uso más frecuente de let es con ?.: el bloque solo se ejecuta si el valor no es null.
fun main() {
// let: dentro, el objeto es it; devuelve lo que calcula el bloque
val letras = "Rayuela".let { it.length }
println(letras)
// ?.let: el bloque solo se ejecuta si el valor NO es null
val titulo: String? = null
titulo?.let { println("Título: $it") } // aquí no se ejecuta: titulo es null
val otro: String? = "Niebla"
otro?.let { println("Título: $it") }
// also: dentro, el objeto es it; devuelve el propio objeto. Ideal para «mirar de paso»
val igual = "abc".also { println("traza $it") }
println(igual)
}7
Título: Niebla
traza abc
abcLas funciones de alcance se encadenan: cada una devuelve algo, y el punto siguiente actúa sobre eso. Como apply y also devuelven el propio objeto, puedes configurarlo con apply y, justo detrás, mirarlo de paso con also, todo en un solo gesto:
fun main() {
// apply monta el texto y devuelve el StringBuilder; el punto siguiente (also) recibe ese mismo objeto
val texto = StringBuilder().apply {
append("hola")
append(" mundo")
}.also { println("traza: $it") } // la traza no cambia nada: texto sigue siendo el StringBuilder
println(texto)
}traza: hola mundo
hola mundoReto · Ejercicio: el café de la cafetería
En el Kotlin Playground, fuera de tu proyecto. Lo difícil no es escribirlo, sino elegir qué función de alcance encaja en cada caso: no todas valen.
- Una clase
Cafecon dos propiedades que se pueden cambiar:tamano(un texto que empieza siendo «pequeño») yazucares(un entero que empieza en 0). Escríbelas dentro de las llaves de la clase, con su valor inicial, en vez de en el constructor. - Crea un café y, en el mismo gesto en que lo creas, ponle el tamaño «grande» y 2 azúcares. Quédate con ese café. De paso, encadenando otra función de alcance justo detrás (como en el ejemplo de
applyyalso), imprime la traza[traza] café preparadosin cambiar nada del café. - Hay una nota opcional del cliente (un texto que puede ser
null). ImprimeNota:seguido del texto, solo si hay nota. Pruébalo con la nota «sin hielo» y con una notanull(la segunda no imprime nada). - Convierte el café en una frase con su tamaño y sus azúcares e imprímela.
La salida tiene que ser:
[traza] café preparado
Nota: sin hielo
grande con 2 azúcaresVer solución
class Cafe {
// propiedades dentro del cuerpo de la clase, cada una con su valor inicial
var tamano = "pequeño"
var azucares = 0
}
fun main() {
// apply: configura el café recién creado y te lo devuelve; también (encadenado) also,
// que deja pasar el mismo café y solo sirve para la traza
val cafe = Cafe().apply {
tamano = "grande" // dentro de apply, this es el café
azucares = 2
}.also { println("[traza] café preparado") }
// let con ?.: el bloque solo se ejecuta si la nota NO es null
val nota: String? = "sin hielo"
nota?.let { println("Nota: $it") }
val sinNota: String? = null
sinNota?.let { println("Nota: $it") } // no imprime nada
// let sin ?.: transforma el café en otra cosa (un texto) y devuelve ese texto
val frase = cafe.let { "${it.tamano} con ${it.azucares} azúcares" }
println(frase)
}La regla para elegir: si quieres quedarte con el objeto (configurarlo o mirarlo de paso), apply y also; si quieres otra cosa hecha a partir del objeto, o actuar solo cuando no es null, let. Entre apply y also: apply para cambiar el objeto (dentro es this), also para mirarlo sin tocarlo (dentro es it).
Paso · Un resumen de equipo y el torneo con apply
Objetivo. Añadir resumenDeEquipo a Formato.kt, que usa las cinco funciones de alcance, y crear el torneo en main con apply. Al ejecutar ./gradlew :torneo:run aparecerá, entre otras líneas, Rayo FC tiene 3 jugadores. Capitán: Ana (#9) (2 con dorsal).
Abre Formato.kt y Main.kt con doble clic en torneo → kotlin+java → com.sebastiannarvaez.marcador.torneo. Los bloques de código llevan como título la ruta del fichero en tu proyecto.
Intenta escribirlo tú. Añade a Formato.kt la función suelta resumenDeEquipo(equipo), que devuelve Rayo FC tiene 3 jugadores. Capitán: Ana (#9) (2 con dorsal) (sin capitán, la segunda frase es Sin capitán) y además imprime la línea [traza] resumen de Rayo FC listo. Una condición: monta su texto usando las cinco funciones de alcance, una por una; con menos, el ejercicio pierde su sentido aunque el resultado coincida. Una pista por función:
let: para la parte del capitán, que solo existe si hay capitán (equipo.capitanpuede sernull).with: para la primera frase, con el nombre y el número de jugadores del equipo.apply: para ir montando el texto entero en unStringBuilderrecién creado.also: para la línea[traza], sin cambiar el texto.run: para el último toque, añadir los «con dorsal» al texto ya montado (toString()convierte elStringBuilderen texto normal).
Después, en main, crea el torneo y registra sus tres partidos en el mismo gesto (configurar el objeto recién creado) e imprime el resumen del Rayo FC. La salida debe quedar así:
Pos Equipo PJ G E P GF GC DG Pts
1 Rayo FC 2 2 0 0 3 1 2 6
2 Toros 2 1 0 1 2 2 0 3
3 Lobos 2 0 0 2 0 2 -2 0
Ana (#9): 3
Iván (#7): 2
[traza] resumen de Rayo FC listo
Rayo FC tiene 3 jugadores. Capitán: Ana (#9) (2 con dorsal)
Capitán de Toros: sin capitánQué se sustituye y dónde:
Formato.kt: la función nueva va al final del fichero, después detablaComoTexto.Main.kt: sustituye desde el comentario// Colecciones y extensiones: una jornada completa en un torneo.hasta la líneaprintln("Capitán de Toros: …")(incluida) por el bloque de más abajo. La líneaprintln("Partidos en el torneo: …")y todo lo que va después se queda como estaba.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Formato.kt, la función nueva al final:
// Las cinco funciones de alcance (scope functions) en una misma función:
// let -> `it` y devuelve el resultado de la lambda
// run -> `this` y devuelve el resultado de la lambda
// with -> `this` (el objeto va como argumento) y devuelve el resultado
// apply -> `this` y devuelve EL OBJETO
// also -> `it` y devuelve EL OBJETO
fun resumenDeEquipo(equipo: Equipo): String {
val jugadoresConDorsal = equipo.plantilla.filter { it.dorsal != null }
val lineaDeCapitan = equipo.capitan?.let { "Capitán: ${it.etiqueta()}" } ?: "Sin capitán"
val texto = with(equipo) { "$nombre tiene $cantidadDeJugadores jugadores" }
return StringBuilder()
.apply {
append(texto)
append(". ")
append(lineaDeCapitan)
}
.also { println("[traza] resumen de ${equipo.nombre} listo") }
.run { toString() + " (${jugadoresConDorsal.size} con dorsal)" }
}Main.kt, el bloque de la jornada, que ahora usa apply:
// Colecciones y extensiones: una jornada completa en un torneo.
val lobos = Equipo("Lobos", listOf(Jugador("Pedro", 4)))
// `apply`: configura el objeto recién creado y lo devuelve.
val torneo = Torneo(nombreDelTorneo, listOf(rayo, toros, lobos)).apply {
agregar(alFinal)
agregar(Partido(toros, lobos).registrar(Gol(20, ivan, toros)))
agregar(Partido(lobos, rayo).registrar(Gol(40, ana, rayo)))
}
println(torneo.tablaComoTexto())
torneo.goleadores().forEach { (jugador, goles) -> println("${jugador.etiqueta()}: $goles") }
println(resumenDeEquipo(rayo))
println("Capitán de Toros: ${toros.capitanONinguno}")Cuando el código compile, entrega el test. FormatoTest.kt es nuevo (5 tests, uno por cada pieza de Formato.kt) y comprueba lo que acabas de escribir y lo de la lección anterior. No enseña nada nuevo, así que se copia con el botón.
- En la vista Android, clic derecho sobre el paquete
com.sebastiannarvaez.marcador.torneo (test)→ New → Kotlin Class/File → File → escribeFormatoTesty Intro. - Pulsa Copiar fichero en el bloque de
FormatoTest.kt. - Dentro del editor, selecciona todo y pega: Cmd+A / Cmd+V. El texto copiado trae su propio
package.
Qué hace cada pieza nueva, en una línea:
| Pieza | Qué demuestra |
|---|---|
resumenDeEquipo(...) | Las cinco funciones de alcance seguidas, cada una con un comentario de qué recibe y qué devuelve |
Torneo(...).apply { … } en main | Configurar un objeto recién creado y quedarte con él |
Punto de control
- En la Terminal de Android Studio (View → Tool Windows → Terminal, que se abre en la raíz del proyecto),
./gradlew :torneo:test :torneo:runtermina enBUILD SUCCESSFUL. Si un test falla, Gradle lo dice conFAILEDy no llega aBUILD SUCCESSFUL. En total pasan 59 tests (54 de antes más los 5 nuevos deFormatoTest). - La salida contiene la tabla con cabecera y tres filas, la primera
1 Rayo FC 2 2 0 0 3 1 2 6. - Después de la tabla salen
Ana (#9): 3yIván (#7): 2. - Aparecen
[traza] resumen de Rayo FC listoy, justo debajo,Rayo FC tiene 3 jugadores. Capitán: Ana (#9) (2 con dorsal). - La última línea de esa parte es
Capitán de Toros: sin capitán. - En la vista Android, el paquete
(test)tiene ahora tambiénFormatoTest.kt, junto a los ficheros de test que ya tenías (TablaTest.ktentre ellos).
Comprueba lo que entendiste
6 preguntas · necesitas 5 para completar la lección
1.Quieres crear un
StringBuilder, añadirle dos textos y quedarte con el propioStringBuilder. ¿Qué función de alcance encaja?2.¿Qué diferencia hay entre
alsoylet?3.Tienes
val titulo: String? = nully escribestitulo?.let { println("Título: $it") }. ¿Qué pasa?4.Dentro de un
applyquieres usar la propiedadcapitandel objeto, pero fuera hay una variable también llamadacapitan. ¿Cómo dejas claro que es la del objeto?5.Quieres calcular un texto con las propiedades de un objeto, sin repetir su nombre, y quedarte con el texto (no con el objeto). ¿Cuál encaja?
6.Tienes un objeto y quieres imprimir una línea de traza en mitad de una cadena de llamadas, sin cambiar nada del objeto ni de lo que devuelve la cadena. ¿Qué función encaja?