Colecciones y key paths
Al terminar: Usar array, set y diccionario, recorrerlos, transformarlos y ordenarlos con key paths.
Al terminar tendrás la tabla de posiciones de Marcador también en Swift: Rayo FC primero con 6 puntos, Toros con 3 y Lobos con 0, con las mismas columnas y el mismo texto que imprimió tu versión de Kotlin.
Esta lección es un repaso con orden. En las anteriores ya usaste algunas de estas piezas (map, filter, reduce, Set, diccionarios, flatMap y las rutas \.nombre) sin pararte a explicarlas. Hoy se explican bien: las tres colecciones de Swift (lista, conjunto y diccionario), las operaciones que las encadenan y las rutas de propiedad (key paths). Con ellas escribes la tabla; las propiedades calculadas que aparecen en el paso (diferencia) se explican a fondo en la lección siguiente.
Tres colecciones
| Swift | Kotlin | Se pregunta por… | Repetidos |
|---|---|---|---|
[Partido] (Array) | List<Partido> | posición: lista[0] | sí |
Set<Equipo> | Set<Equipo> | pertenencia | no |
[Equipo: Int] (Dictionary) | Map<Equipo, Int> | clave: mapa[clave] | valores sí, claves no |
Tres diferencias con Kotlin. No hay versión «mutable» y otra «de solo lectura»: el tipo es el mismo, con let no cambia y con var, sí («Si asignas un array, un set o un dictionary a una constante, esa colección es inmutable», dice la documentación). Son tipos de valor: copiar un array copia el array (struct frente a class). Y el orden de un Set y de un Dictionary cambia entre ejecuciones: si necesitas un orden, usa sorted.
Las operaciones que encadenan
Ninguna cambia la colección original: devuelven una nueva. Reciben closures (Closures): filter { $0 > 1 }.
| Swift | Qué hace | Kotlin | Con [3, 1, 2, 1] |
|---|---|---|---|
map { … } | transforma cada elemento | map | [6, 2, 4, 2] con $0 * 2 |
filter { … } | se queda con los que cumplen | filter | [3, 2] con $0 > 1 |
reduce(0) { total, g in … } | junta todo en un valor, con valor inicial | fold | 7 con reduce(0, +) |
compactMap { … } | transforma y descarta los nil | mapNotNull | ver abajo |
count { … } | cuenta los que cumplen | count | 2 con $0 == 1 |
Dictionary(grouping:by:) | agrupa en un diccionario | groupBy | ver abajo |
reduce de Swift siempre recibe el valor inicial, así que en una colección vacía devuelve ese valor y no falla (el reduce de Kotlin lanzaba una excepción). reduce(0, +) funciona porque + es una función de dos números y se puede pasar tal cual.
Un diccionario tiene además tres cosas propias: values (solo los valores), keys (solo las claves) y, al recorrerlo o ordenarlo, cada elemento es una pareja con key (la clave) y value (el valor). Como un diccionario no tiene orden, para recorrerlo en orden se usa sorted, que devuelve un array de parejas. En for (nombre, edad) in …, los paréntesis abren cada pareja en dos constantes, una por cada parte (es una tupla, que se explica un poco más abajo).
let goles = [3, 1, 2, 1]
print(goles.map { $0 * 2 }) // [6, 2, 4, 2]
print(goles.filter { $0 > 1 }) // [3, 2]
print(goles.reduce(0, +)) // 7
// `Int("diez")` devuelve `Int?`: nil si el texto no es un número (Opcionales).
// `compactMap` convierte y filtra a la vez: se queda con los que no son nil.
let textos = ["10", "diez", "7"]
print(textos.map { Int($0) }) // [Optional(10), nil, Optional(7)]
print(textos.compactMap { Int($0) }) // [10, 7]
// `first` de un texto es un `Character?`; `?? "?"` da un valor por defecto.
let porInicial = Dictionary(grouping: ["Ana", "Iván", "Alba"], by: { $0.first ?? "?" })
print(porInicial["A"] ?? []) // ["Ana", "Alba"]
// Buscar en un diccionario devuelve un opcional; `default:` es «y si no está, esto».
print(porInicial["Z", default: []]) // []: la clave no existe
// Un diccionario de edades: values, keys y recorrerlo en orden.
let edades = ["Ana": 30, "Iván": 25]
print(edades.values.reduce(0, +)) // 55: values, solo los valores
print(edades.keys.sorted()) // ["Ana", "Iván"]: keys, solo las claves
for (nombre, edad) in edades.sorted(by: { $0.key < $1.key }) {
print(nombre, edad) // cada elemento es una pareja: .key y .value
}[6, 2, 4, 2]
[3, 2]
7
[Optional(10), nil, Optional(7)]
[10, 7]
["Ana", "Alba"]
[]
55
["Ana", "Iván"]
Ana 30
Iván 25Rutas de propiedad (key paths)
Hasta ahora escribías { $0.nombre } para sacar la propiedad de cada elemento. Una ruta de propiedad (key path) es esa misma idea como un valor: \.nombre significa «la propiedad nombre de lo que llegue».
struct Jugador {
let nombre: String
let dorsal: Int?
}
let jugadores = [Jugador(nombre: "Ana", dorsal: 9), Jugador(nombre: "Luis", dorsal: nil)]
print(jugadores.map { $0.nombre }) // ["Ana", "Luis"]
print(jugadores.map(\.nombre)) // ["Ana", "Luis"]: lo mismo, más corto
print(jugadores.compactMap(\.dorsal))// [9]: quita el nil de Luis["Ana", "Luis"]
["Ana", "Luis"]
[9]Se pueden encadenar (\.equipo.nombre) y guardar en una constante. En Kotlin el equivalente es Jugador::nombre.
Juntar las piezas: una extensión que agrupa y no repite
La tabla de posiciones usa casi todo lo anterior a la vez, y alguna pieza más. Antes de escribirla, las piezas juntas en otro dominio: los kilómetros que suma cada ciudad en una lista de viajes. Las piezas nuevas:
extension Array where Element == Viaje: añade métodos solo a los arrays de un tipo concreto, sin tocarArray(Protocolos y extensiones, con una condición). Ya pegaste una igual,goleadores, en some y any.flatMap { … }: la closure devuelve un array por elemento yflatMaplos une en uno. Aquí cada viaje da dos apariciones, una por cada ciudad.- Tupla con nombres:
(ciudad: "Cali", km: 390)se lee con.ciudady.km. Es elTriplede Kotlin, con etiquetas (y es lo que descomponía elfor (nombre, edad)de arriba). vistas.insert(ciudad).inserted:inserten unSetdice coninsertedsi el elemento era nuevo; con eso, el bucle se queda con cada ciudad una sola vez y conserva el orden.ciudades + porCiudad.keysune el array de ciudades con las claves del diccionario.
// Un tramo entre dos ciudades.
struct Viaje {
let origen: String
let destino: String
let km: Int
}
// Solo los arrays de Viaje reciben este método.
extension Array where Element == Viaje {
func kmPorCiudad(ciudades: [String] = []) -> [String: Int] {
// Cada viaje da DOS apariciones, una por ciudad: flatMap une los arrays en uno.
let apariciones = flatMap { viaje in
[
(ciudad: viaje.origen, km: viaje.km), // tupla con nombres: se lee con .ciudad y .km
(ciudad: viaje.destino, km: viaje.km),
]
}
let porCiudad = Dictionary(grouping: apariciones, by: \.ciudad) // ciudad → sus apariciones
// insert(…).inserted es true solo la primera vez: así no se repite ninguna ciudad.
var vistas = Set<String>()
var todas: [String] = []
for ciudad in ciudades + porCiudad.keys { // + une el array con las claves
if vistas.insert(ciudad).inserted { todas.append(ciudad) }
}
var resultado: [String: Int] = [:]
for ciudad in todas {
let suyas = porCiudad[ciudad] ?? [] // una ciudad sin viajes: lista vacía
resultado[ciudad] = suyas.map(\.km).reduce(0, +)
}
return resultado
}
}
let viajes = [Viaje(origen: "Cali", destino: "Pasto", km: 390), Viaje(origen: "Cali", destino: "Buga", km: 75)]
let tabla = viajes.kmPorCiudad(ciudades: ["Tuluá"])
print(tabla["Cali"] ?? 0, tabla["Tuluá"] ?? -1) // 465 0: Tuluá sale aunque no tenga viajes465 0Reto · Ejercicio: goleadores con reduce, sorted y grouping
En tu Playground, parte de este diccionario de goles por jugador:
let goles = ["Ana": 3, "Iván": 2, "Marta": 2, "Luis": 1]- Imprime el total de goles, juntando todos los valores del diccionario en un solo número.
- Imprime la lista de nombres de más a menos goles; si dos empatan, van en orden alfabético. Pista: la ordenación puede recibir una comparación que mira dos elementos a la vez, cada uno con su clave y su valor.
- Agrupa los nombres por número de goles e imprime cada grupo en una línea, con los goles delante y los nombres ordenados, de más a menos goles.
Lo que debe salir:
8
["Ana", "Iván", "Marta", "Luis"]
3 ["Ana"]
2 ["Iván", "Marta"]
1 ["Luis"]Ver solución
let goles = ["Ana": 3, "Iván": 2, "Marta": 2, "Luis": 1]
print(goles.values.reduce(0, +)) // 8
let ranking = goles
.sorted { a, b in
if a.value != b.value { return a.value > b.value }
return a.key < b.key
}
.map(\.key)
print(ranking) // ["Ana", "Iván", "Marta", "Luis"]
let porGoles = Dictionary(grouping: goles.keys, by: { goles[$0] ?? 0 })
for (cantidad, nombres) in porGoles.sorted(by: { $0.key > $1.key }) {
print(cantidad, nombres.sorted()) // 3 ["Ana"], luego 2 ["Iván", "Marta"], luego 1 ["Luis"]
}Con una closure de dos parámetros, el desempate cabe en una pasada. El diccionario se ordena al imprimirlo, no al crearlo.
Paso 1 · Crea Tabla.swift
Objetivo. Un fichero nuevo en tu paquete Torneo con el struct que representa una fila de la tabla y con tablaDePosiciones, una función sobre [Partido] que devuelve una fila por equipo, ordenada. Compila, y todavía no se imprime.
Los ficheros viven en ~/Marcador-Swift/Torneo.
Crea el fichero.
- En el Project navigator (el panel de la izquierda de Xcode; se muestra u oculta con Cmd+1), despliega
Sources, clic derecho sobre la carpetaTorneo(la de dentro deSources) → New File from Template… → Swift File → Next, escribeTablay pulsa Create. Si el menú de tu versión de Xcode se llama distinto, crea el fichero desde la Terminal, en~/Marcador-Swift/Torneo, contouch Sources/Torneo/Tabla.swift. - Comprueba que el fichero quedó dentro de
Sources/Torneo.
Escribe su contenido. Primero, un struct con ocho datos guardados y uno calculado (diferencia): una propiedad calculada no guarda nada y se evalúa cada vez que se lee, como cantidadDeJugadores en struct frente a class. La lección siguiente las explica a fondo.
Intenta escribirlo tú. Todo el código de arriba te sirve: es el del ejemplo de los viajes con otros datos.
-
FilaDePosicion: un struct público que se pueda comparar con==(adoptaEquatable), con las propiedades públicas de solo lecturaequipo,jugados,ganados,empatados,perdidos,golesAFavor,golesEnContraypuntos, y una calculada,diferencia: los goles a favor menos los goles en contra. -
tablaDePosiciones: un método público que solo tienen las listas de partidos. Recibe:- con la etiqueta
equipos, los equipos que deben salir aunque no hayan jugado (ninguno si no se dan); - con la etiqueta
reglamento, el reglamento de puntos (el de liga si no se da).
Devuelve las filas de la tabla:
- cada partido cuenta dos veces, una por equipo, con sus goles a favor y en contra;
- cada equipo sale una sola vez, primero los dados y después los demás, con sus partidos jugados, ganados, empatados y perdidos, sus goles a favor y en contra y sus puntos según el reglamento;
- se ordena por puntos y, si empatan, por diferencia de goles, después por goles a favor y por último por nombre, con el
Rankingde Genéricos en Swift. Aquí no hace faltaComparable: elRankingrecibe la comparación de desempate que le das.
- con la etiqueta
Los tests de la lección siguiente exigen ese nombre y esas etiquetas. Si te ayuda, empieza imprimiendo cada paso. La función sigue cinco pasos, como en Kotlin: flatMap, Dictionary(grouping:by:) con \.equipo, un Set que une los equipos sin repetir, map con count y reduce para cada fila, y el Ranking.
Qué se escribe y dónde. Todo en Tabla.swift: FilaDePosicion arriba y la extensión debajo.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// Una fila de la tabla de posiciones. Un struct con propiedades guardadas y una
// CALCULADA (`diferencia`): no ocupa espacio, se evalúa cada vez que se lee.
public struct FilaDePosicion: Equatable {
public let equipo: Equipo
public let jugados: Int
public let ganados: Int
public let empatados: Int
public let perdidos: Int
public let golesAFavor: Int
public let golesEnContra: Int
public let puntos: Int
public var diferencia: Int { golesAFavor - golesEnContra }
}// Extensión de `Array` restringida a `Element == Partido`: añade un método a
// `[Partido]` sin tocar Array (la `List<Partido>.tablaDePosiciones()` de Kotlin).
// `equipos` permite que salgan también los que aún no han jugado.
extension Array where Element == Partido {
public func tablaDePosiciones(
equipos: [Equipo] = [],
reglamento: any Reglamento = ReglamentoLiga()
) -> [FilaDePosicion] {
// Cada partido cuenta dos veces: una desde el lado local y otra desde el
// visitante. Cada elemento es una tupla con nombres: (equipo, aFavor, enContra).
let apariciones = flatMap { partido in
[
(equipo: partido.local, aFavor: partido.golesLocal, enContra: partido.golesVisitante),
(equipo: partido.visitante, aFavor: partido.golesVisitante, enContra: partido.golesLocal),
]
}
// `Dictionary(grouping:by:)`: agrupa por clave. `\.equipo` es un KEY PATH:
// una referencia a la propiedad, equivalente a `{ $0.equipo }`. Es
// Dictionary<Equipo, [(equipo:, aFavor:, enContra:)]> (el `groupBy` de Kotlin).
let porEquipo = Dictionary(grouping: apariciones, by: \.equipo)
// Un Set no admite repetidos: `insert` dice con `inserted` si el elemento
// era nuevo. Así se unen los equipos dados y los que ya jugaron, sin repetir.
var vistos = Set<Equipo>()
var todos: [Equipo] = []
for equipo in equipos + porEquipo.keys {
if vistos.insert(equipo).inserted { todos.append(equipo) }
}
let filas = todos.map { equipo in
let marcadores = porEquipo[equipo] ?? []
return FilaDePosicion(
equipo: equipo,
jugados: marcadores.count,
ganados: marcadores.count { $0.aFavor > $0.enContra },
empatados: marcadores.count { $0.aFavor == $0.enContra },
perdidos: marcadores.count { $0.aFavor < $0.enContra },
golesAFavor: marcadores.map(\.aFavor).reduce(0, +),
golesEnContra: marcadores.map(\.enContra).reduce(0, +),
puntos: marcadores.reduce(0) { total, marcador in
total + reglamento.puntosPor(golesAFavor: marcador.aFavor, golesEnContra: marcador.enContra)
}
)
}
// La tabla es un Ranking<FilaDePosicion> puntuado por los puntos de la liga.
return Ranking(
elementos: filas,
puntos: \.puntos,
desempate: { a, b in
if a.diferencia != b.diferencia { return a.diferencia > b.diferencia }
if a.golesAFavor != b.golesAFavor { return a.golesAFavor > b.golesAFavor }
return a.equipo.nombre < b.equipo.nombre
}
).ordenados
}
}Compila. En la Terminal, cd ~/Marcador-Swift/Torneo y luego xcrun swift build. Debe terminar con Build complete!. En Xcode, el mismo resultado es Product → Build (Cmd+B).
Paso 2 · Imprimir la tabla
Objetivo. Ver la tabla en la consola. Primero Torneo aprende a darla, después llega el fichero que alinea las columnas y por último el bloque de main.swift que la imprime. Al terminar, xcrun swift run torneo-cli muestra la tabla de Rayo FC, Toros y Lobos.
Torneo.swift: pedir la tabla al torneo
El torneo ya tiene sus partidos, sus equipos y su reglamento: la tabla es llamar a la función que acabas de escribir con esos tres datos. Intenta escribirlo tú: en Torneo (Sources → Torneo), justo debajo del init, un método público tablaDePosiciones() sin parámetros que devuelva la tabla de sus partidos con sus equipos y su reglamento.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
public func tablaDePosiciones() -> [FilaDePosicion] {
partidos.tablaDePosiciones(equipos: equipos, reglamento: reglamento)
}Formato.swift: alinear las columnas
Imprimir la tabla es alinear texto en columnas (el "%2d %-8s …".format(…) de Kotlin), y eso no es el tema de la lección: Formato.swift se rellena con el botón Copiar fichero, que copia el contenido al portapapeles y no escribe nada en tu proyecto (se explica en Clases y objetos).
Ojo: se pega entero y sustituye al que ya tienes. El fichero existe desde Equatable y Hashable y el contenido nuevo trae lo que ya tenía (etiqueta() de Jugador y capitanONinguno de Equipo) más lo nuevo: relleno, aTexto(puesto:) y tablaComoTexto(). Si añadiste algo tuyo a ese fichero, guárdalo antes en otro. Depende de FilaDePosicion y de tablaDePosiciones(), así que cópialo ahora que ya existen.
- Abre
Formato.swift(Sources → Torneo) con doble clic. - Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
- Haz clic dentro del fichero, selecciona todo con Cmd+A y pega con Cmd+V.
main.swift
En main.swift (Sources → torneo-cli) el bloque nuevo va en medio de dos líneas que ya tienes, y por eso el bloque de abajo las trae: la primera línea (print("Ranking de textos: …"), la última de la lección de Genéricos en Swift) y la última (let torneo = Torneo(…), que hiciste en registrar lanza errores: throws tipado y Result) no se escriben, solo marcan dónde empieza y dónde acaba lo nuevo. Pega, o escribe, lo que hay entre ellas. Lo nuevo usa lobos, alFinal y partidoDeToros, que ya están declarados más arriba. Si lo escribes tú: crea la copa con los tres equipos y registra en ella alFinal, partidoDeToros y un Lobos contra Rayo con un gol de Ana para el Rayo en el minuto 40 (si falla, «No se pudo registrar:» y el error); imprime la tabla como texto. La copa tiene así tres partidos; el torneo de la línea siguiente (el que se guarda después) sigue teniendo dos. Lo que debes ver:
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 0Intenta primero. Cuando termines, compara con la solución.Mostrar el código
print("Ranking de textos: \(porLongitud.ordenados)")
// Colecciones: la tabla de posiciones de una jornada completa.
let copa = Torneo(nombre: nombreDelTorneo, equipos: [rayo, toros, lobos])
do {
try copa.registrar(alFinal)
try copa.registrar(partidoDeToros)
try copa.registrar(Partido(local: lobos, visitante: rayo).registrando(.gol(minuto: 40, jugador: ana, equipo: rayo)))
} catch {
print("No se pudo registrar: \(error.localizedDescription)")
}
print(copa.tablaComoTexto())
let torneo = Torneo(nombre: nombreDelTorneo, equipos: [rayo, toros, lobos])Ahora compila y ejecuta, desde ~/Marcador-Swift/Torneo:
xcrun swift build
xcrun swift run torneo-cliPunto de control
xcrun swift build(desde~/Marcador-Swift/Torneo) termina sin errores.xcrun swift run torneo-cliimprime, entre otras, estas líneas:
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 0xcrun swift testtermina conTest run with 69 tests in 9 suites passed: los mismos de la lección anterior, porque hoy no hay tests nuevos.- Provócalo: mueve tu bloque de
main.swiftpor encima delet lobos = …: la tabla sale con dos filas y sin error. Devuélvelo a su sitio.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Tienes
let textos = ["10", "diez", "7"]. ¿Qué devuelvetextos.compactMap { Int($0) }?2.¿Qué es
\.nombreenjugadores.map(\.nombre)?3.En
main.swiftescribesprint("antes: \(despues)")y, una línea más abajo,let despues = 5. ¿Qué ocurre?4.Tienes
let edades = ["Ana": 30, "Iván": 25]. Enfor (nombre, edad) in edades.sorted(by: { $0.key < $1.key }), ¿qué es cada elemento que ordenasorted?5.En
var vistos = Set<Equipo>(), ¿qué indicavistos.insert(equipo).inserted?