Saltar al contenido
K
Kotlin: el lenguajePaso a paso25 min de lectura · 15 min de máquina Intermedio

Enum y when exhaustivo

Al terminar: Modelar un conjunto cerrado de valores con enum y leerlo con when, y dejar que el compilador compruebe que no falta ningún caso.

Al terminar esta lección sabrás decirle a Kotlin «esta variable solo puede valer una de estas cosas» y dejar que el compilador te avise si olvidas una. Es la primera pieza con la que Marcador describirá lo que pasa en un partido: aquí creas el color de las tarjetas, y en la lección siguiente, los eventos. Temas: enum class, constantes con datos, when exhaustivo y el error de poner else.

Enum: un conjunto fijo de valores

A veces una variable solo puede valer una de unas pocas cosas conocidas: la luz de un semáforo (roja, amarilla o verde), el día de la semana, la talla de una camiseta. Si las guardas como texto ("rojo"), un error de escritura ("rojp") compila y falla después. Un enum (de «enumeración») es un tipo cuyos únicos valores posibles los listas tú, y Kotlin no admite ningún otro.

Cada valor es una constante: un valor con nombre que nunca cambia. Por convención se escriben en MAYÚSCULAS. Se usan con el nombre del enum, un punto y la constante: Semaforo.ROJO.

Para decidir según el valor se usa when, que es un «según esto, haz aquello»: cada rama es valor -> resultado. Un when puede dar un resultado, y entonces se escribe después del = de una función, como en accion del ejemplo.

ejemplo mínimo: un enum con when (con comentarios)
// enum class: declara el tipo y sus únicos valores posibles
enum class Semaforo { ROJO, AMARILLO, VERDE }
 
// when sobre el enum: una rama por valor. Como los tres valores están cubiertos,
// no hace falta else: el compilador sabe que no existe ningún otro.
fun accion(luz: Semaforo): String = when (luz) {
    Semaforo.ROJO -> "Para"
    Semaforo.AMARILLO -> "Frena"
    Semaforo.VERDE -> "Pasa"
}
 
fun main() {
    val luz = Semaforo.AMARILLO
    println(luz)                                    // println escribe el nombre de la constante
    println(accion(luz))                            // la rama que corresponde a AMARILLO
    for (l in Semaforo.entries) {                   // entries: la lista de todas las constantes, en orden
        println("${l.name}: ${accion(l)}")          // name: el nombre de la constante, como texto
    }
}

Al ejecutarlo, imprime:

AMARILLO
Frena
ROJO: Para
AMARILLO: Frena
VERDE: Pasa

Exhaustivo: el compilador cuenta por ti

Un when que se usa como valor (= when …, como el de accion) tiene que dar siempre un resultado, así que tiene que cubrir todos los casos. Eso se llama que sea exhaustivo. Con un tipo normal, como un número, es imposible listarlos todos y tienes que cerrar con una rama else («en cualquier otro caso»). Con un enum no hace falta: la lista es finita y el compilador la conoce. Y si te dejas uno, no compila. Quita la rama VERDE de accion y dice:

'when' expression must be exhaustive. Add the 'VERDE' branch or an 'else' branch.

Es la gran ventaja: si mañana añades APAGADO al enum, el compilador te marca cada when que todavía no lo contempla.

Un dato en cada constante

Un enum también puede llevar un dato en cada constante. Se escribe como un constructor (lo viste en Clases y objetos), y cada constante pasa su dato entre paréntesis:

ejemplo mínimo: un enum con datos (con comentarios)
// Cada constante lleva una etiqueta para mostrar
enum class Talla(val etiqueta: String) {
    PEQUENA("Pequeña"),       // PEQUENA pasa "Pequeña" como etiqueta
    MEDIANA("Mediana"),
    GRANDE("Grande"),
}
 
fun main() {
    println(Talla.MEDIANA.etiqueta)                 // el punto lee el dato de la constante
    for (talla in Talla.entries) {
        println("${talla.name} -> ${talla.etiqueta}")
    }
}
Mediana
PEQUENA -> Pequeña
MEDIANA -> Mediana
GRANDE -> Grande

Una limitación: todas las constantes tienen la misma forma

Fíjate en Talla: las tres constantes llevan el mismo dato (una etiqueta), y cada una existe una sola vez: Talla.MEDIANA es siempre el mismo valor. Eso basta para tallas, colores o días. No basta cuando cada opción necesita datos distintos —un gol necesita quién marcó; una tarjeta, a quién y de qué color—. Para eso Kotlin tiene los tipos sellados, que son la lección siguiente: Sealed: estados con datos.

Error típico: el else que apaga el aviso

Lo primero que se escribe cuando el when protesta es else. Compila, y deja de proteger:

con el bug: else en un enum
fun descripcion(luz: Semaforo): String = when (luz) {
    Semaforo.ROJO -> "Para"
    else -> "Sigue"   // ¿y AMARILLO? Cae aquí sin avisar
}

Con Semaforo.VERDE devuelve Sigue, bien, pero con Semaforo.AMARILLO también, y ahí habría que frenar. Peor: si mañana añades APAGADO al enum, este when sigue compilando y lo trata como «Sigue». El fallo aparece en la calle, no en el compilador. Sin else, cada constante nueva rompe todos los when que la ignoran:

corregido: una rama por constante, sin else
fun descripcion(luz: Semaforo): String = when (luz) {
    Semaforo.ROJO -> "Para"
    Semaforo.AMARILLO -> "Frena"
    Semaforo.VERDE -> "Sigue"
}

Si quitas una rama de la versión corregida, el compilador dice 'when' expression must be exhaustive y sugiere else como salida. Ignora esa segunda opción: la primera es la buena.

En el módulo de Swift verás su equivalente: allí un enum con su switch sin default hace de enum con when sin else.

Reto · Ejercicio: la dificultad de un ejercicio

Fuera de tu proyecto, en el Kotlin Playground (https://play.kotlinlang.org).

Un libro de problemas marca cada ejercicio como fácil (1 punto), media (3 puntos) o difícil (5 puntos). Crea un enum Dificultad con esas tres constantes (FACIL, MEDIA, DIFICIL), cada una con sus puntos. Escribe una función consejo que reciba una dificultad y devuelva un texto con un when sin rama por defecto: «Calienta» para la fácil, «Concentrate» para la media y «Sin prisa» para la difícil. Después, en main, recorre todas las constantes y escribe una línea por cada una. Por último, añade una cuarta, EXPERTO con 8 puntos, sin tocar consejo: anota qué dice el compilador y arréglalo. Antes de añadir EXPERTO, la consola debe mostrar:

FACIL -> 1 puntos: Calienta
MEDIA -> 3 puntos: Concentrate
DIFICIL -> 5 puntos: Sin prisa
Ver solución
solución: la dificultad de un ejercicio
enum class Dificultad(val puntos: Int) {
    FACIL(1),
    MEDIA(3),
    DIFICIL(5),
}
 
fun consejo(d: Dificultad): String = when (d) {
    Dificultad.FACIL -> "Calienta"
    Dificultad.MEDIA -> "Concentrate"
    Dificultad.DIFICIL -> "Sin prisa"
}
 
fun main() {
    for (d in Dificultad.entries) {
        println("${d.name} -> ${d.puntos} puntos: ${consejo(d)}")
    }
}

Al añadir EXPERTO(8), el compilador dice 'when' expression must be exhaustive. Add the 'EXPERTO' branch or an 'else' branch. y señala el when de consejo. Lo correcto es añadir la rama Dificultad.EXPERTO -> "Respira" (o el texto que quieras), no el else.

Paso · El color de las tarjetas

Objetivo. Crear EventoDePartido.kt con el enum ColorDeTarjeta y comprobar en main que cada color conoce su etiqueta. Al terminar, ./gradlew :torneo:run imprime dos líneas más y ./gradlew :torneo:test sigue pasando con 24 tests.

torneo/
kotlin+java/
com.sebastiannarvaez.marcador.torneo
EventoDePartido.ktse crea
Main.ktse modifica
Lo que tocas, en la vista Android: un fichero nuevo y un cambio en Main.kt.

Crea el fichero. 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 → File → escribe EventoDePartido y pulsa Intro. Es el mismo gesto de Tu primer proyecto en Kotlin.

Piezas nuevas de este paso, todas vistas arriba: un enum con un dato por constante y entries.

Intenta escribirlo tú:

  1. El color. Una tarjeta es amarilla o roja, y cada color sabe su nombre para mostrarlo («Amarilla», «Roja»). Los nombres exactos: ColorDeTarjeta, AMARILLA, ROJA y etiqueta.
  2. Probarlo en main. Al final de main, recorre los colores y escribe una línea por cada uno.

Tras las líneas de la lección anterior (la última es Con otro dorsal: …), la salida debe acabar así:

AMARILLA -> Amarilla
ROJA -> Roja

Cuando termines, abre el bloque y compara con tu código. Los bloques con ruta llevan la del fichero en tu proyecto.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/EventoDePartido.kt
package com.sebastiannarvaez.marcador.torneo
 
// `enum class`: un conjunto cerrado de valores, todos con el mismo aspecto.
// Cada constante puede llevar datos (aquí, la etiqueta para mostrar).
enum class ColorDeTarjeta(val etiqueta: String) {
    AMARILLA("Amarilla"),
    ROJA("Roja"),
}

Pega esto en lugar de la última } de main en Main.kt (la } va incluida abajo; el resto del fichero no cambia). El … del bloque solo marca el código que ya tienes: no lo escribas. Es provisional: la lección siguiente lo sustituye.

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Main.kt
    …
 
    // Provisional: los colores de tarjeta.
    for (color in ColorDeTarjeta.entries) {
        println("${color.name} -> ${color.etiqueta}")
    }
}

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 dos últimas líneas de la salida son las del punto de control de abajo. Los tests no cambian en este paso: ./gradlew :torneo:test sigue pasando con los mismos 24.

Punto de control

  • ./gradlew :torneo:run termina en BUILD SUCCESSFUL y las dos últimas líneas de la salida son:
AMARILLA -> Amarilla
ROJA -> Roja
  • ./gradlew :torneo:test pasa. Abre el informe (open torneo/build/reports/tests/test/index.html) y comprueba que pone 24 tests en total: 5 de PuntosTest, 6 de JugadorTest, 7 de EquipoTest y 6 de PartidoTest.
  • EventoDePartido.kt solo contiene el enum ColorDeTarjeta.
  • Partido no ha cambiado: sigue guardando golesLocal y golesVisitante.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.Tienes enum class Semaforo { ROJO, AMARILLO, VERDE } y un when (luz) usado como valor con tres ramas, una por constante y sin else. ¿Qué pasa si borras la rama de VERDE?

  2. 2.Tu when sobre ColorDeTarjeta termina en else -> "Otra" y mañana añades AZUL al enum. ¿Qué pasa?

  3. 3.Con enum class Talla(val etiqueta: String) { PEQUENA("Pequeña"), GRANDE("Grande") }, ¿qué imprime println(Talla.GRANDE.etiqueta)?

  4. 4.¿Qué te da Semaforo.entries?

  5. 5.Cada opción necesita datos distintos (un gol, quién marcó; una tarjeta, a quién y de qué color). ¿Sirve un enum class?

Faltan 5 de 5