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

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:

ejemplo mínimo: repetir el nombre frente a abrir un bloque (con comentarios)
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ónUso típicoDentro del bloque el objeto esDevuelve
applyconfigurar un objeto recién creadothisel propio objeto
also«mirar de paso» (una traza) sin cambiar nadaitel propio objeto
lethacer algo solo si no es null, o transformar un valoritlo que devuelve el bloque
runcalcular algo usando las propiedades del objetothislo que devuelve el bloque
with(objeto) { }lo mismo que run, con el objeto entre paréntesisthislo 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.

ejemplo mínimo: run, with y apply (con comentarios)
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
ab

Las 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.

ejemplo mínimo: let y also (con comentarios)
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
abc

Las 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:

ejemplo mínimo: apply y also encadenados (con comentarios)
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 mundo

Reto · 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.

  1. Una clase Cafe con dos propiedades que se pueden cambiar: tamano (un texto que empieza siendo «pequeño») y azucares (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.
  2. 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 apply y also), imprime la traza [traza] café preparado sin cambiar nada del café.
  3. Hay una nota opcional del cliente (un texto que puede ser null). Imprime Nota: seguido del texto, solo si hay nota. Pruébalo con la nota «sin hielo» y con una nota null (la segunda no imprime nada).
  4. 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úcares
Ver solución
solución: el café de la cafetería
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).

torneo/
kotlin+java → com.sebastiannarvaez.marcador.torneo
Formato.ktse modifica
Main.ktse modifica
kotlin+java → com.sebastiannarvaez.marcador.torneo (test)
FormatoTest.ktse crea (se copia)
Dos ficheros que cambian y un test nuevo. Los nodos son los de la vista Android.

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.capitan puede ser null).
  • with: para la primera frase, con el nombre y el número de jugadores del equipo.
  • apply: para ir montando el texto entero en un StringBuilder recié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 el StringBuilder en 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án

Qué se sustituye y dónde:

  1. Formato.kt: la función nueva va al final del fichero, después de tablaComoTexto.
  2. Main.kt: sustituye desde el comentario // Colecciones y extensiones: una jornada completa en un torneo. hasta la línea println("Capitán de Toros: …") (incluida) por el bloque de más abajo. La línea println("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:

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Formato.kt
// 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:

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Main.kt
    // 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.

  1. En la vista Android, clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (test) → New → Kotlin Class/File → File → escribe FormatoTest y Intro.
  2. Pulsa Copiar fichero en el bloque de FormatoTest.kt.
  3. Dentro del editor, selecciona todo y pega: Cmd+A / Cmd+V. El texto copiado trae su propio package.
FormatoTest.kt

Qué hace cada pieza nueva, en una línea:

PiezaQué demuestra
resumenDeEquipo(...)Las cinco funciones de alcance seguidas, cada una con un comentario de qué recibe y qué devuelve
Torneo(...).apply { … } en mainConfigurar 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:run termina en BUILD SUCCESSFUL. Si un test falla, Gradle lo dice con FAILED y no llega a BUILD SUCCESSFUL. En total pasan 59 tests (54 de antes más los 5 nuevos de FormatoTest).
  • 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): 3 y Iván (#7): 2.
  • Aparecen [traza] resumen de Rayo FC listo y, 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én FormatoTest.kt, junto a los ficheros de test que ya tenías (TablaTest.kt entre ellos).

Comprueba lo que entendiste

6 preguntas · necesitas 5 para completar la lección

  1. 1.Quieres crear un StringBuilder, añadirle dos textos y quedarte con el propio StringBuilder. ¿Qué función de alcance encaja?

  2. 2.¿Qué diferencia hay entre also y let?

  3. 3.Tienes val titulo: String? = null y escribes titulo?.let { println("Título: $it") }. ¿Qué pasa?

  4. 4.Dentro de un apply quieres usar la propiedad capitan del objeto, pero fuera hay una variable también llamada capitan. ¿Cómo dejas claro que es la del objeto?

  5. 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. 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?

Faltan 6 de 6