Bucles y rangos: for, while y etiquetas
El protocolo de iteración sobre el que se construye for, rangos y progresiones con step y downTo, while y do while, break y continue con etiquetas, y la razón de diseño por la que Kotlin eliminó el for clásico de tres partes.
Kotlin no tiene el for de tres partes que arrastran C, Java y casi todos sus descendientes. En su lugar hay un for que recorre cualquier cosa iterable y un sistema de rangos que cubre con precisión el noventa por ciento de lo que aquel bucle hacía. La sustitución no es un capricho estético: cambia qué errores son posibles y qué puede optimizar el compilador.
- Entender que
fores azúcar sobre un protocolo de convención, no una construcción cerrada. - Construir rangos y progresiones con
rangeTo,downTo,stepyuntil. - Usar
while,do while,breakycontinuecon etiquetas. - Argumentar por qué desapareció el
forclásico de tres partes.
for es azúcar sobre un protocolo
El for de Kotlin no conoce ningún tipo en particular. Recorre cualquier expresión que ofrezca una función iterator —miembro o extensión, marcada como operator— cuyo resultado tenga hasNext y next. Es tipado estructural por convención, resuelto en compilación:
for (x in coleccion) usar(x)
// se traduce, conceptualmente, a esto:
val it = coleccion.iterator()
while (it.hasNext()) {
val x = it.next()
usar(x)
}
La consecuencia es que puedes hacer iterable un tipo ajeno sin tocarlo, definiendo la extensión adecuada:
operator fun ResultSet.iterator(): Iterator<ResultSet> = object : Iterator<ResultSet> {
override fun hasNext(): Boolean = next()
override fun next(): ResultSet = this@iterator
}
Sobre esa base, el lenguaje añade comodidades que evitan el índice manual: indices da el rango de posiciones válidas, withIndex produce pares de posición y valor, y las entradas de un mapa se desestructuran directamente en la cabecera del bucle.
for (i in lista.indices) print(lista[i])
for ((i, v) in lista.withIndex()) print("$i vale $v")
for ((clave, valor) in mapa) print("$clave es $valor")
Rangos y progresiones
Un rango es un intervalo; una progresión es un rango recorrido con un paso. 1..10 construye un IntRange, que ya es una IntProgression de paso uno; aplicarle step o downTo produce otra progresión.
for (i in 1..10) { } // cerrado por ambos extremos: 1 a 10
for (i in 1..<10) { } // abierto por la derecha: 1 a 9
for (i in 10 downTo 1) { } // descendente
for (i in 1..10 step 3) { } // 1, 4, 7, 10
for (c in 'a'..'e') { } // también sobre Char
La forma 1..<10 con el operador de rango abierto es la preferida desde Kotlin 1.9 frente a la vieja función infija until, porque hace visible en la sintaxis dónde está el extremo excluido.
Una progresión guarda tres datos —first, last y step— y su last no es el límite que escribiste, sino el último elemento realmente alcanzable. Es un detalle con consecuencias:
val p = 1..9 step 3
println(p.last) // 7, no 9: la progresión es 1, 4, 7
println(p.isEmpty()) // false
println((5..1).isEmpty()) // true: un rango descendente sin downTo está vacío
Ese último caso es la trampa clásica: 5..1 no recorre nada, no lanza nada y no avisa de nada. Si el sentido del recorrido depende de datos, usa downTo explícitamente o comprueba el orden antes.
Los rangos también sirven fuera del bucle, como predicado de pertenencia con in, incluso sobre tipos que no son iterables:
if (edad in 18..65) admitir()
if (medida in 0.0..1.0) normalizar() // Double: comparable, no iterable
if (letra !in 'a'..'z') rechazar()
El compilador reconoce el patrón for sobre un rango o progresión de tipos enteros y genera un bucle con una variable de índice, sin crear el objeto ni el iterador. El coste es idéntico al del bucle de tres partes de Java. Si el rango se guarda antes en una variable de tipo Iterable, la optimización se pierde.
while, break, continue y etiquetas
while y do while funcionan como cabría esperar, y siguen siendo la herramienta correcta cuando la condición de parada no es un recorrido. En Kotlin, además, la variable declarada dentro de un do while sigue siendo visible en su condición:
do {
val linea = lector.readLine()
procesar(linea)
} while (linea != null) // linea es visible aquí
break y continue actúan sobre el bucle más interno, y para salir de otro se usan etiquetas: un identificador seguido de arroba delante del bucle, y el mismo identificador tras el break.
buscar@ for (fila in tablero) {
for (celda in fila) {
if (celda.esMina) break@buscar // sale de los dos bucles
if (celda.vacia) continue@buscar // siguiente fila
}
}
El mismo mecanismo se reutiliza en los lambdas, donde return desnudo dentro de una función inline sale de la función envolvente —el llamado retorno no local— y la forma etiquetada sale solo del lambda:
lista.forEach {
if (it == 0) return@forEach // equivale a continue
procesar(it)
}
Por qué no hay for clásico de tres partes
El for de tres partes mezcla tres responsabilidades sin relación: inicializar, comprobar y avanzar. Nada obliga a que la variable comprobada sea la que avanza, ni a que la comprobación termine alguna vez. De ahí salen los errores más antiguos de la profesión: el desfase por uno, la variable de índice modificada dentro del cuerpo, la condición que usa una variable y el avance que incrementa otra.
Recorrer posiciones
Donde había un índice manual, ahora hay for (i in lista.indices), y el rango se deriva de la colección en lugar de escribirse a mano.
Repetir n veces
Cuando el índice ni siquiera se usa, repeat(n) expresa exactamente la intención sin declarar variable alguna.
Avance irregular
Si el avance no es una progresión, la respuesta honesta es while: hace visible que hay estado propio en juego.
flowchart TD A[Quiero repetir algo] --> B[Recorro una secuencia conocida] B -->|si, una coleccion| C[for sobre el iterable] B -->|si, numeros regulares| D[for sobre rango o progresion] B -->|solo cuento vueltas| E[repeat] B -->|no, la parada depende del estado| F[while o do while] style C fill:#a6e3a1,color:#11111b style D fill:#a6e3a1,color:#11111b style F fill:#f9e2af,color:#11111b
Al restringir el for al recorrido de una secuencia conocida, Kotlin gana dos cosas. La variable del bucle es siempre un val de nuevo en cada vuelta, así que no puede alterarse por accidente ni provocar capturas compartidas en lambdas. Y como la forma del recorrido es explícita —un rango, una progresión, un iterable—, el compilador puede reconocerla y generar el código óptimo.
Renunciar al for de tres partes parece perder expresividad, y en sentido estricto la pierde: hay bucles que ya no se escriben con for. Pero lo que se cede es precisamente lo que impedía razonar. Un bucle de tres partes es un while disfrazado con una variable mutable a la vista de todos; su forma no dice nada sobre cuántas vueltas dará ni sobre qué recorre, así que ni el compilador ni el lector pueden anticipar nada. Un for sobre un iterable declara la intención completa —esta secuencia, entera, un elemento cada vez— y esa declaración es lo que habilita todo lo demás: la eliminación del iterador para rangos enteros, la variable de bucle inmutable que se puede capturar sin sorpresas en un lambda, y sobre todo la continuidad con la biblioteca de colecciones, donde map, filter o sumOf operan sobre la misma abstracción. Cuando la iteración es un valor y no una plantilla sintáctica, se puede pasar, componer, hacer perezosa con Sequence o suspender en una corrutina. El bucle de tres partes no admite nada de eso, porque nunca fue una abstracción: era una macro sobre saltos.
- Imprime
(1..9 step 3).lasty explica por qué no vale 9. - Escribe un bucle sobre
5..1y comprueba que no ejecuta nada; corrígelo condownTo. - Usa una etiqueta para salir de un bucle doble en cuanto encuentres el primer elemento que cumpla una condición.
- Convierte un
forcon índice manual enwithIndexy luego enrepeat, y compara los tres. - Investiga: define
operator fun iterator()como extensión de una clase tuya y recórrela confor.