Enums con valores asociados
Al terminar: Modelar eventos con enums con valores asociados y raw values.
Al terminar esta lección Marcador tiene en Swift los tipos que guardan lo que pasa en un partido —goles, tarjetas, cambios— y su resultado, igual que en tu versión de Kotlin. La pieza clave es el enum de Swift, que es más potente que el de otros lenguajes: cada caso puede llevar sus propios datos. Con él, EventoDePartido sale en una sola declaración y el compilador te avisa si olvidas un caso. En la lección siguiente, Enums: el partido de eventos, Partido los usa para deducir el marcador.
Temas: enums con valores asociados, valores brutos (raw values), CaseIterable y switch exhaustivo sobre un enum.
Un enum, tres usos
Un enum es un tipo que solo puede valer uno de unos casos que escribes tú. En Swift esos casos pueden ser de tres clases, y la diferencia es qué guardan:
| Forma | Qué guarda cada caso | Ejemplo en Marcador |
|---|---|---|
| Casos simples | Nada | .empate |
| Valores brutos (raw values) | Un valor fijo, del mismo tipo para todos, escrito al declarar el enum | ColorDeTarjeta.roja vale "ROJA" |
| Valores asociados (associated values) | Datos distintos en cada caso, que se dan al crear el valor | .gol(minuto: 12, jugador: ana, equipo: rayo) |
Una diferencia que conviene tener clara: el valor bruto se fija al declarar el enum y es el mismo siempre (.roja es "ROJA" en cualquier parte). El valor asociado se da cada vez que creas el caso (un gol en el 12' y otro en el 80' son valores distintos del mismo caso).
Dónde se prueba. Los ejemplos mínimos y el ejercicio de esta lección se prueban en tu Playground de Xcode, el que aprendiste a crear y a ejecutar en Tu primer paquete de Swift. Reutiliza el mismo Playground: borra lo que tenía de la lección anterior, pega el código y ejecútalo. Los ejemplos de esta lección se pegan uno detrás de otro en él, salvo cuando se dice lo contrario.
enum Pago { // un Pago solo puede ser uno de estos tres casos
case efectivo(monto: Int) // valor asociado: cada pago en efectivo trae su monto
case tarjeta(monto: Int, ultimos4: String) // este caso trae dos datos
case gratis // este no trae ninguno
}
func recibo(_ pago: Pago) -> String {
switch pago { // sin default: una rama por caso
case let .efectivo(monto): "Efectivo: \(monto)" // let abre el dato y lo llama monto
case let .tarjeta(monto, ultimos4): "Tarjeta \(ultimos4): \(monto)" // dos datos, por orden
case .gratis: "Gratis" // sin datos que abrir
}
}
enum Talla: String, CaseIterable { // String: cada caso tiene un valor bruto de texto
case pequena = "S" // el valor bruto se fija aquí, una vez
case grande = "L"
} // CaseIterable: Swift crea Talla.allCases
print(recibo(.tarjeta(monto: 30, ultimos4: "4242"))) // el punto basta: Swift sabe que es un Pago
print(Talla.grande.rawValue) // L
print(Talla(rawValue: "S") == .pequena) // del texto al caso: devuelve un Talla?
print(Talla(rawValue: "XL") == nil) // ningún caso vale "XL": nil
print(Talla.allCases.count) // 2
print(Talla.allCases.map(\.rawValue)) // ["S", "L"]: el valor bruto de cada casoImprime Tarjeta 4242: 30, L, true, true, 2 y ["S", "L"]. Cinco cosas para leer con calma:
recibo(.tarjeta(monto: 30, ultimos4: "4242")): se escribe.tarjeta(…)sin el nombre del tipo delante. Cuando Swift ya sabe qué tipo espera (aquí, unPago), acepta el punto inicial en lugar dePago.tarjeta(…). Lo verás en todo el módulo.case let .tarjeta(monto, ultimos4):abre el valor: liga los datos del caso a dos constantes que se usan en esa rama. Es lo que hacía el smart cast de Kotlin (is Tarjeta -> evento.ultimos4). Fíjate en que al abrirlo no se escriben las etiquetas (monto:,ultimos4:): los nombres que pones los das tú y se asignan por orden, igual que encase let .efectivo(monto). Si no necesitas un dato, ponle_.- Cada rama del
switches una expresión y elswitchentero devuelve un valor, sinreturn: como elwhende Kotlin usado como expresión, y como elswitchde Funciones y switch. Talla: Stringda un valor bruto de tipo texto a cada caso. Swift crea ademásTalla(rawValue:), que devuelve un opcional:nilsi el texto no corresponde a ningún caso. En Kotlin, para ir del texto al valor existevalueOf, que no llegaste a usar: si el texto no corresponde, lanza una excepción..map(\.rawValue)transforma cada elemento de una lista: aquí, cada talla pasa a su valor bruto.\.rawValuees un key path (una forma corta de decir «de cada elemento, surawValue», equivale a{ $0.rawValue });mapy los key paths se ven a fondo en Colecciones y key paths. Hoy basta con leerlo así.
CaseIterable es un protocolo (un contrato: una lista de cosas que un tipo promete ofrecer; lo verás a fondo en Protocolos y extensiones) que pide una sola cosa y la da hecha: allCases, la lista de todos los casos.
Error típico: el default que apaga el aviso
Como el compilador conoce todos los casos, un switch sobre un enum tiene que cubrirlos todos: es la misma garantía que el when sin else de Kotlin, y se apaga de la misma manera. Lo primero que se escribe cuando el compilador protesta es default. Compila, y deja de proteger. Con números o textos, que tienen infinitos valores, default es la salida correcta (como en Funciones y switch); con un enum propio, lo correcto es una rama por caso. Para probarlo, en el mismo Playground, sustituye la función recibo del ejemplo anterior por esta (deja el enum Pago):
func recibo(_ pago: Pago) -> String {
switch pago {
case let .efectivo(monto): "Efectivo: \(monto)"
case let .tarjeta(monto, _): "Tarjeta: \(monto)"
default: "Otro" // ¿y los pagos gratis? Caen aquí sin avisar
}
}Si mañana se añade el caso .reembolso(monto: Int), esta función sigue compilando y los reembolsos se describen como «Otro». El fallo aparece en pantalla, no en el compilador. Sin default, cada caso nuevo rompe todos los switch que lo ignoran:
func recibo(_ pago: Pago) -> String {
switch pago {
case let .efectivo(monto): "Efectivo: \(monto)"
case let .tarjeta(monto, _): "Tarjeta: \(monto)"
case .gratis: "Gratis"
}
}Añade ahora case reembolso(monto: Int) al enum y deja la función como está: el compilador dice (y además te propone el caso que falta):
error: switch must be exhaustive
note: add missing case: '.reembolso(monto: let monto)'| Idea | Kotlin (el que escribiste en el módulo de Kotlin) | Swift |
|---|---|---|
| Conjunto fijo de valores | enum class ColorDeTarjeta(val etiqueta: String) | enum ColorDeTarjeta: String, CaseIterable |
| Variantes con datos distintos | sealed interface EventoDePartido + data class Gol(...) | enum EventoDePartido con case gol(minuto:…) |
Dato común (minuto) | val minuto: Int en la interfaz | propiedad calculada con un switch |
| Variante sin datos | data object Empate | case empate |
| Ligar los datos de una variante | is Gol -> y smart cast | case let .gol(minuto, jugador, _) |
| Todos los valores | ColorDeTarjeta.entries | ColorDeTarjeta.allCases |
| Del texto al valor | valueOf (no lo usaste; lanza una excepción) | init?(rawValue:) (opcional) |
| Comprobación de casos | when sin else | switch sin default |
Las piezas que vas a escribir
Para el proyecto hacen falta estas piezas, todas sobre enums:
enum ColorDeTarjeta: String, CaseIterableconcase amarilla = "AMARILLA": valores brutos de tipo texto yallCases. Son los mismos textos que tu versión de Kotlin escribía en el JSON, el fichero de texto donde guardaba el torneo; en Swift lo leerás más adelante.case gol(minuto: Int, jugador: Jugador, equipo: Equipo): un caso con valores asociados con nombre.case .gol(let minuto, _, _), .tarjeta(let minuto, _, _), .cambio(let minuto, _, _): minuto: varios patrones en una sola rama, separados por comas, ligandominutoen cada uno;_ignora lo que no interesa. Sustituye alval minutode la interfaz de Kotlin.- Propiedades calculadas dentro de un enum (
var etiqueta: String { switch self { … } }):selfes el caso actual. Y.lowercased(), que pasa un texto a minúsculas. EquatableenEventoDePartido(un contrato que dice «dos valores se pueden comparar con==»): para que compile, todo lo que guardan sus casos tiene que serEquatable. Por esoJugadorlo es desde Opcionales yEquipodesde struct frente a class. La lección siguiente lo usa para comparar equipos.
Las tres últimas, juntas y comentadas, en otro dominio (las entradas de un cine; no es tu código de Marcador). Pégalo en tu Playground:
enum Entrada: Equatable { // Equatable: lo que guardan los casos (Int) ya lo es
case adulto(dia: Int, precio: Int)
case nino(dia: Int, precio: Int)
case invitado(dia: Int)
// Propiedad calculada dentro de un enum: `self` es el caso actual.
var dia: Int {
switch self {
// Tres patrones en una rama, separados por comas; cada uno liga `dia` y `_` ignora el resto.
case .adulto(let dia, _), .nino(let dia, _), .invitado(let dia):
dia
}
}
var etiqueta: String {
switch self {
case .adulto: "Adulto"
case .nino: "Niño"
case .invitado: "Invitado"
}
}
}
let entrada = Entrada.nino(dia: 6, precio: 8)
print(entrada.dia) // 6
print(entrada.etiqueta.lowercased()) // niño: lowercased() pasa el texto a minúsculas
print(entrada == .nino(dia: 6, precio: 8)) // true: con Equatable se compara con ==
print(entrada == .invitado(dia: 6)) // falseImprime 6, niño, true y false.
Reto · Ejercicio: un pedido con estados
En tu Playground (borra antes lo anterior):
- Crea un enum para el estado de un pedido, con tres casos: pendiente (sin datos), en camino (con el nombre del repartidor) y entregado (con los minutos que tardó).
- Escribe una función que reciba un estado y devuelva un texto para cada caso: «Esperando repartidor», «Va Rosa» o «Llegó en 25 minutos», sin rama por defecto.
- Añade un cuarto caso, cancelado, con el motivo, sin tocar la función. Anota qué dice el compilador y arréglalo: el texto es «Cancelado:» seguido del motivo.
- Crea otro enum, la prioridad, con los casos baja, media y alta, que valgan 1, 2 y 3 como enteros y que permita recorrer todos sus casos. Imprime la lista de esos enteros, si la prioridad que vale 2 es la media y si no existe ninguna que valga 9.
Imprime también el mensaje de un pedido que lleva Rosa y el de uno cancelado por «sin stock». Lo que debe salir, por este orden:
Va Rosa
Cancelado: sin stock
[1, 2, 3]
true
trueVer solución
enum EstadoDePedido {
case pendiente
case enCamino(repartidor: String)
case entregado(minutos: Int)
case cancelado(motivo: String)
}
func mensaje(_ estado: EstadoDePedido) -> String {
switch estado {
case .pendiente: "Esperando repartidor"
case let .enCamino(repartidor): "Va \(repartidor)"
case let .entregado(minutos): "Llegó en \(minutos) minutos"
case let .cancelado(motivo): "Cancelado: \(motivo)"
}
}
enum Prioridad: Int, CaseIterable {
case baja = 1
case media = 2
case alta = 3
}
print(mensaje(.enCamino(repartidor: "Rosa")))
print(mensaje(.cancelado(motivo: "sin stock")))
print(Prioridad.allCases.map(\.rawValue))
print(Prioridad(rawValue: 2) == .media)
print(Prioridad(rawValue: 9) == nil)Antes de añadir la rama de .cancelado, el compilador dice switch must be exhaustive y propone add missing case. Lo correcto es añadir la rama, no un default.
Al ejecutarlo imprime Va Rosa, Cancelado: sin stock, [1, 2, 3], true y true.
Paso · Los tres tipos de los eventos, en Swift
Objetivo. Crear EventoDePartido.swift con ColorDeTarjeta, EventoDePartido y Resultado, que usará Partido en la lección siguiente. Al terminar, el paquete compila y sus 28 tests siguen pasando.
Los ficheros de código viven en tu carpeta ~/Marcador-Swift/Torneo. El título de cada bloque es la ruta del fichero dentro de ~/Marcador-Swift, la misma que ves en el árbol de arriba.
Crear EventoDePartido.swift. En el Project navigator (el panel de la izquierda de Xcode; se muestra u oculta con Cmd+1), despliega Sources, haz clic derecho sobre la carpeta Torneo (la de dentro de Sources) → New File from Template… → Swift File → Next, escribe EventoDePartido y pulsa Create. Si el menú de tu versión de Xcode se llama distinto, crea el fichero desde la Terminal, en ~/Marcador-Swift/Torneo: touch Sources/Torneo/EventoDePartido.swift. Comprueba que el fichero queda dentro de Sources/Torneo y no en otra carpeta: el objetivo al que pertenece un fichero lo decide la carpeta donde está, como viste en Tu primer paquete de Swift.
Lo que tienen que ofrecer los tipos. Los tests de la lección siguiente usan estos nombres y textos exactos:
| Tipo | Qué lleva |
|---|---|
enum ColorDeTarjeta: String, CaseIterable | amarilla = "AMARILLA", roja = "ROJA" y etiqueta: String ("Amarilla", "Roja") |
enum EventoDePartido: Equatable | Tres casos: gol(minuto:jugador:equipo:), tarjeta(minuto:jugador:color:) y cambio(minuto:sale:entra:); y minuto: Int, que vale el minuto de cualquiera de ellos |
describir(_:) -> String | 12' Gol de Ana (Rayo FC), 30' Tarjeta amarilla para Iván (la etiqueta en minúsculas) y 85' Cambio: sale Ana, entra Luis |
enum Resultado: Equatable | victoria(ganador:perdedor:), empate y titular: String: Gana Rayo FC o Empate |
Intenta escribirlo tú, en este orden:
ColorDeTarjeta, con dos casos, amarilla y roja, cuyo valor es el texto «AMARILLA» o «ROJA», que se pueden recorrer todos, y unaetiqueta(«Amarilla», «Roja»).EventoDePartido, con tres casos y sus datos: un gol (minuto, jugador y equipo), una tarjeta (minuto, jugador y color) y un cambio (minuto, quién sale y quién entra), y una propiedad,minuto, que da el de cualquiera de ellos.describir, que recibe un evento sin etiqueta y lo convierte en una línea como las de arriba.Resultado: una victoria (con el ganador y el perdedor) o un empate, con untitular, «Gana Rayo FC» o «Empate».
Todavía nada usa estos tipos, así que no hay salida nueva que ver: lo que debes ver al compilar es Build complete!. Cuando termines, abre el bloque y compara con la solución.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
public enum ColorDeTarjeta: String, CaseIterable {
case amarilla = "AMARILLA"
case roja = "ROJA"
// Un enum puede tener propiedades calculadas y métodos.
public var etiqueta: String {
switch self {
case .amarilla: "Amarilla"
case .roja: "Roja"
}
}
}
// `enum` con VALORES ASOCIADOS: cada caso lleva sus propios datos, distintos de
// los de los demás. Es el gemelo del `sealed interface` de Kotlin, con
// `Gol`, `Tarjeta` y `Cambio` como casos en vez de clases aparte.
//
// El compilador sabe cuáles son TODOS los casos: un `switch` sin `default` que
// olvide uno no compila (en Kotlin, el `when` sin `else`).
public enum EventoDePartido: Equatable {
case gol(minuto: Int, jugador: Jugador, equipo: Equipo)
case tarjeta(minuto: Int, jugador: Jugador, color: ColorDeTarjeta)
case cambio(minuto: Int, sale: Jugador, entra: Jugador)
// En Kotlin, `val minuto` estaba en la interfaz y cada clase lo sobrescribía.
// Aquí se saca con un `switch` que liga solo lo que interesa: `_` ignora el resto.
public var minuto: Int {
switch self {
case .gol(let minuto, _, _), .tarjeta(let minuto, _, _), .cambio(let minuto, _, _):
minuto
}
}
}
// `switch` sobre un enum: sin `default`. Si mañana se añade un caso, esta función
// deja de compilar y avisa de dónde falta.
public func describir(_ evento: EventoDePartido) -> String {
switch evento {
case let .gol(minuto, jugador, equipo):
"\(minuto)' Gol de \(jugador.nombre) (\(equipo.nombre))"
case let .tarjeta(minuto, jugador, color):
"\(minuto)' Tarjeta \(color.etiqueta.lowercased()) para \(jugador.nombre)"
case let .cambio(minuto, sale, entra):
"\(minuto)' Cambio: sale \(sale.nombre), entra \(entra.nombre)"
}
}
// Un partido acaba en victoria (de alguien) o en empate. `.victoria` lleva
// datos; `.empate` no necesita ninguno (el `data object` de Kotlin).
public enum Resultado: Equatable {
case victoria(ganador: Equipo, perdedor: Equipo)
case empate
public var titular: String {
switch self {
case let .victoria(ganador, _): "Gana \(ganador.nombre)"
case .empate: "Empate"
}
}
}Punto de control
- Build. Desde
~/Marcador-Swift/Torneo,xcrun swift buildtermina enBuild complete!. - Run.
xcrun swift run torneo-cliimprime lo mismo que en la lección anterior:main.swifttodavía no usa los tipos nuevos. - Test.
xcrun swift testtermina conTest run with 28 tests in 4 suites passed: los mismos que antes, sin ninguno nuevo. - Provocado. Quita de
describirla rama de.cambio:xcrun swift buildfalla conswitch must be exhaustive. Devuélvela. - Sabes decir por qué
describirytitularno llevandefault, y qué diferencia hay entreColorDeTarjeta.roja.rawValuey los datos de.gol(minuto: 12, …).
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.¿Cuál es la diferencia entre un valor asociado y un valor bruto (raw value) de un enum?
2.Tienes
enum Talla: String { case pequena = "S"; case grande = "L" }. ¿Qué devuelveTalla(rawValue: "XL")?3.¿Qué pasa con este
switchsobreEventoDePartidosi falta la rama.cambioy no haydefault?4.¿Por qué conviene no poner
defaulten unswitchsobre un enum propio?5.En Kotlin
EventoDePartidoera unasealed interfacecon unadata classpor variante. ¿Cómo se expresa lo mismo en Swift?