Saltar al contenido
K
Swift: el lenguajePaso a paso35 min de lectura · 10 min de máquina Intermedio

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:

FormaQué guarda cada casoEjemplo en Marcador
Casos simplesNada.empate
Valores brutos (raw values)Un valor fijo, del mismo tipo para todos, escrito al declarar el enumColorDeTarjeta.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.

ejemplo mínimo: valores asociados y valores brutos
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 caso

Imprime 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í, un Pago), acepta el punto inicial en lugar de Pago.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 en case let .efectivo(monto). Si no necesitas un dato, ponle _.
  • Cada rama del switch es una expresión y el switch entero devuelve un valor, sin return: como el when de Kotlin usado como expresión, y como el switch de Funciones y switch.
  • Talla: String da un valor bruto de tipo texto a cada caso. Swift crea además Talla(rawValue:), que devuelve un opcional: nil si el texto no corresponde a ningún caso. En Kotlin, para ir del texto al valor existe valueOf, 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. \.rawValue es un key path (una forma corta de decir «de cada elemento, su rawValue», equivale a { $0.rawValue }); map y 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):

con el bug: default en un enum
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:

corregido: un caso por rama, sin default
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)'
IdeaKotlin (el que escribiste en el módulo de Kotlin)Swift
Conjunto fijo de valoresenum class ColorDeTarjeta(val etiqueta: String)enum ColorDeTarjeta: String, CaseIterable
Variantes con datos distintossealed interface EventoDePartido + data class Gol(...)enum EventoDePartido con case gol(minuto:…)
Dato común (minuto)val minuto: Int en la interfazpropiedad calculada con un switch
Variante sin datosdata object Empatecase empate
Ligar los datos de una varianteis Gol -> y smart castcase let .gol(minuto, jugador, _)
Todos los valoresColorDeTarjeta.entriesColorDeTarjeta.allCases
Del texto al valorvalueOf (no lo usaste; lanza una excepción)init?(rawValue:) (opcional)
Comprobación de casoswhen sin elseswitch sin default

Las piezas que vas a escribir

Para el proyecto hacen falta estas piezas, todas sobre enums:

  • enum ColorDeTarjeta: String, CaseIterable con case amarilla = "AMARILLA": valores brutos de tipo texto y allCases. 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, ligando minuto en cada uno; _ ignora lo que no interesa. Sustituye al val minuto de la interfaz de Kotlin.
  • Propiedades calculadas dentro de un enum (var etiqueta: String { switch self { … } }): self es el caso actual. Y .lowercased(), que pasa un texto a minúsculas.
  • Equatable en EventoDePartido (un contrato que dice «dos valores se pueden comparar con ==»): para que compile, todo lo que guardan sus casos tiene que ser Equatable. Por eso Jugador lo es desde Opcionales y Equipo desde 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:

ejemplo mínimo: varios patrones, propiedades calculadas y Equatable en un enum
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))                 // false

Imprime 6, niño, true y false.

Reto · Ejercicio: un pedido con estados

En tu Playground (borra antes lo anterior):

  1. 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ó).
  2. 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.
  3. 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.
  4. 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
true
Ver solución
solución: pedido con estados y prioridad
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.

Torneo
Sources
Torneo
EventoDePartido.swiftse crea
Lo que tocas, en el Project navigator de Xcode con el paquete Torneo abierto. Un solo fichero nuevo.

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:

TipoQué lleva
enum ColorDeTarjeta: String, CaseIterableamarilla = "AMARILLA", roja = "ROJA" y etiqueta: String ("Amarilla", "Roja")
enum EventoDePartido: EquatableTres 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(_:) -> String12' 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: Equatablevictoria(ganador:perdedor:), empate y titular: String: Gana Rayo FC o Empate

Intenta escribirlo tú, en este orden:

  1. ColorDeTarjeta, con dos casos, amarilla y roja, cuyo valor es el texto «AMARILLA» o «ROJA», que se pueden recorrer todos, y una etiqueta («Amarilla», «Roja»).
  2. 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.
  3. describir, que recibe un evento sin etiqueta y lo convierte en una línea como las de arriba.
  4. Resultado: una victoria (con el ganador y el perdedor) o un empate, con un titular, «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
Torneo/Sources/Torneo/EventoDePartido.swift
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 build termina en Build complete!.
  • Run. xcrun swift run torneo-cli imprime lo mismo que en la lección anterior: main.swift todavía no usa los tipos nuevos.
  • Test. xcrun swift test termina con Test run with 28 tests in 4 suites passed: los mismos que antes, sin ninguno nuevo.
  • Provocado. Quita de describir la rama de .cambio: xcrun swift build falla con switch must be exhaustive. Devuélvela.
  • Sabes decir por qué describir y titular no llevan default, y qué diferencia hay entre ColorDeTarjeta.roja.rawValue y los datos de .gol(minuto: 12, …).

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Cuál es la diferencia entre un valor asociado y un valor bruto (raw value) de un enum?

  2. 2.Tienes enum Talla: String { case pequena = "S"; case grande = "L" }. ¿Qué devuelve Talla(rawValue: "XL")?

  3. 3.¿Qué pasa con este switch sobre EventoDePartido si falta la rama .cambio y no hay default?

  4. 4.¿Por qué conviene no poner default en un switch sobre un enum propio?

  5. 5.En Kotlin EventoDePartido era una sealed interface con una data class por variante. ¿Cómo se expresa lo mismo en Swift?

Faltan 5 de 5