some y any
Al terminar: Entender some y any con protocolos genéricos.
Ranking<T> ya ordena cualquier cosa. En esta lección los protocolos ganan un hueco de tipo (associatedtype), aprendes la diferencia entre some y any, y usas las dos para sacar la lista de goleadores con su puesto: 1. Ana (#9): 2.
Temas: tipos asociados y some frente a any.
Los ejemplos y el ejercicio se escriben en tu Playground, el de Tu primer paquete de Swift: reutiliza el mismo y borra lo anterior.
Tipos asociados: un hueco dentro de un protocolo
Un protocolo no lleva <T> como una interfaz de Kotlin. Lleva un tipo asociado (associated type): un hueco que cada tipo que adopta el protocolo rellena con el suyo.
protocol Contenedor {
associatedtype Elemento // el hueco: cada tipo dice qué guarda
var todos: [Elemento] { get }
}
struct Nombres: Contenedor { let todos = ["Ana", "Luis"] } // Elemento = String
struct Dorsales: Contenedor { let todos = [9, 7, 4] } // Elemento = Int
// C.Elemento es «el tipo que ese C haya elegido».
func primero<C: Contenedor>(_ c: C) -> C.Elemento? { c.todos.first }
print(primero(Nombres()) as Any)
print(primero(Dorsales()) as Any)Optional("Ana")
Optional(9)primero(Nombres()) devuelve un String? y primero(Dorsales()) un Int?: el compilador sabe cuál en cada llamada.
some frente a any
Hasta aquí, un protocolo era una lista de cosas que un tipo sabe hacer. Para usarlo como tipo (en el tipo de un parámetro, de un valor devuelto o de una variable) Swift pide una palabra delante que diga cómo de concreto es el tipo que hay detrás. Son dos, y se entienden con un ejemplo de cada una.
some: un tipo concreto que decide quien escribe la función. La función promete «te doy algún Contenedor» y devuelve siempre el mismo tipo; el compilador lo sabe, quien llama no necesita saberlo. Usa el Contenedor de arriba, en el mismo Playground:
// Siempre devuelve un `Nombres`, aunque quien llama solo vea «un Contenedor».
func nombresOrdenados() -> some Contenedor { Nombres() }
print(nombresOrdenados().todos) // ["Ana", "Luis"]any: cualquier tipo que cumpla el protocolo, decidido al ejecutar. Sirve para guardar cosas de tipos distintos en el mismo sitio, como una lista que mezcla Nombres y Dorsales:
// Dos tipos distintos en la misma lista: con `some` no se podría.
let mezcla: [any Contenedor] = [Nombres(), Dorsales()]
print(mezcla.map { $0.todos.count }) // [2, 3]Esta tabla resume la diferencia:
some Contenedor | any Contenedor | |
|---|---|---|
| Qué tipo es | Uno concreto, fijo, que decide quien implementa | Cualquiera que cumpla el protocolo, decidido en ejecución |
¿Conserva Elemento? | Sí | No: se pierde el tipo concreto |
| ¿Lista de tipos distintos? | No | Sí: [any Contenedor] |
| Coste | Ninguno | Pequeño: el valor va «en una caja» |
Regla práctica: empieza con some (o con un genérico) y usa any solo cuando necesites mezclar tipos distintos en una misma variable o lista. some en un parámetro es una forma corta de escribir un genérico: func f(_ x: some Contenedor) equivale a func f<C: Contenedor>(_ x: C).
Cuándo any no tiene sustituto. Con some, el tipo concreto es uno solo, y el compilador tiene que poder deducirlo. Hay dos cosas que some no admite, y ahí any es la única salida:
- Una propiedad guardada cuyo valor llega de fuera:
struct Torneo { let reglamento: some Reglamento }no compila (property declares an opaque return type, but has no initializer expression…). Por esoTorneode Protocolos y extensiones guarda unany Reglamento. - Una función que devuelve tipos distintos según el caso:
-> some Reglamentoconb ? ReglamentoLiga() : ReglamentoPersonalizado(…)daresult values in '? :' expression have mismatching types.
Y un detalle de escritura: any se escribe siempre delante de un protocolo usado como tipo. Con un protocolo con tipo asociado, como Contenedor, hoy el compilador solo avisa si falta, pero en un modo futuro del lenguaje será un error.
Kotlin ↔ Swift
| Kotlin (el tuyo) | Swift |
|---|---|
Una interface no lleva <T>; si hace falta un tipo abierto, lo lleva la clase o la función | protocol con associatedtype Elemento |
List<Reglamento> (lista de una interfaz: de cada elemento solo ves la interfaz) | [any Contenedor] |
fun f(): Reglamento (oculta la clase, pero puede devolver otra distinta en cada llamada) | -> some Contenedor: siempre el mismo tipo concreto, que el compilador conoce |
Reto · Ejercicio: colecciones de cromos y monedas
En tu Playground, haz un protocolo para una colección que tenga una lista de piezas de un tipo que decide cada colección. Haz dos colecciones: cromos (los nombres «Ana», «Iván» y «Luis») y monedas (los valores 1, 2, 5 y 10). Escribe tres funciones: la primera pieza de cualquier colección (o nada); cuántas piezas tiene una colección, aceptando «alguna colección» concreta; y el total de piezas de una lista que mezcla colecciones de tipos distintos. Imprime la primera pieza de cada colección, cuántas tiene cada una (en una línea) y el total de las dos. Lo que debe salir:
Ana
1
3 4
7Ver solución
protocol Coleccion {
associatedtype Pieza
var piezas: [Pieza] { get }
}
struct Cromos: Coleccion { let piezas = ["Ana", "Iván", "Luis"] }
struct Monedas: Coleccion { let piezas = [1, 2, 5, 10] }
func primera<C: Coleccion>(_ coleccion: C) -> C.Pieza? { coleccion.piezas.first }
func cuantas(_ coleccion: some Coleccion) -> Int { coleccion.piezas.count }
func total(_ colecciones: [any Coleccion]) -> Int {
var suma = 0
for coleccion in colecciones { suma += coleccion.piezas.count }
return suma
}
print(primera(Cromos()) ?? "ninguna")
print(primera(Monedas()) ?? 0)
print(cuantas(Cromos()), cuantas(Monedas()))
print(total([Cromos(), Monedas()]))Salida: Ana, 1, 3 4 y 7. primera y cuantas aceptan un tipo concreto cada vez (genérico y some); total necesita any porque la lista mezcla dos tipos.
Paso · funciones sobre protocolos y goleadores
Objetivo. Añadir a Ranking.swift el protocolo Clasificacion y las funciones que lo usan (con some y any), pegar los goleadores y probarlo en main.swift. Al ejecutar torneo-cli verás la lista de goleadores con su puesto.
El título de cada bloque de abajo es la ruta del fichero dentro de ~/Marcador-Swift, la misma que ves en el árbol de arriba.
Dónde va cada cosa. Todo lo de Ranking.swift va debajo de lo que ya tienes, una parte debajo de otra, en el orden de esta lección.
Lo que exigen los tests que copiarás. Las funciones lider, nombres (con la etiqueta de) y cuantosPuestosHay. Y goleadores, sobre una lista de partidos y sobre Torneo, que devuelve un ranking de jugadores con desempate por nombre.
Parte 3a · El protocolo Clasificacion y lider(_:)
Piezas nuevas:
associatedtype Elemento: el hueco del protocolo, como enContenedor.typealias Elemento = T: un nombre nuevo para un tipo. Aquí rellena el hueco: «en unRanking<T>, elElementoesT». Yextension Ranking: Clasificaciones la forma de que un tipo ya escrito adopte un protocolo desde fuera.C.Elemento: el tipo que haya elegido elCde cada llamada.
Con el Contenedor de arriba se ve claro: Nombres rellenaba el hueco sin decirlo (Swift lo deducía de [String]). Ranking<T> es genérico, así que aquí se escribe a mano: typealias Elemento = T.
Intenta escribirlo tú: el protocolo público Clasificacion, con un hueco para el tipo de sus elementos, Elemento, y una lista ordenados de solo lectura; que Ranking lo adopte, con sus elementos como Elemento; y la función lider, que recibe cualquier clasificación sin etiqueta y devuelve su primer elemento, o nada si está vacía.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// Un protocolo con un hueco: `Elemento` lo rellena cada tipo que lo adopta.
public protocol Clasificacion {
associatedtype Elemento
var ordenados: [Elemento] { get }
}
// Ranking adopta el protocolo: aquí el hueco `Elemento` se rellena con `T`.
extension Ranking: Clasificacion {
public typealias Elemento = T
}
// Función genérica sobre el protocolo: `C.Elemento` es el tipo que ese `C` haya elegido.
public func lider<C: Clasificacion>(_ clasificacion: C) -> C.Elemento? {
clasificacion.ordenados.first
}Parte 3b · nombres(de:) y cuantosPuestosHay(_:)
Piezas nuevas:
some Sequence<Jugador>:Sequencees el protocolo que pegaste en la lección anterior. Algunos protocolos de Swift dejan decir entre<>de qué tipo son sus elementos:Sequence<Jugador>quiere decir «una secuencia cuyos elementos sonJugador». Consomedelante, la función acepta cualquier secuencia de jugadores (un array, unRanking…) y es una forma corta de escribir un genérico.any Clasificacion: aquí sí hace faltaany, porque la lista mezcla clasificaciones de tipos distintos (el caso del apartado anterior).reduce(0) { $0 + $1.ordenados.count }: va acumulando un valor a partir del inicial (0), elemento a elemento; aquí suma cuántos elementos tiene cada clasificación.
Las piezas, con el Contenedor de arriba:
// some Sequence<String>: cualquier secuencia de textos (un array, un Set…).
func enMayusculas(_ palabras: some Sequence<String>) -> [String] {
palabras.map { $0.uppercased() }
}
// [any Contenedor]: una lista que mezcla Nombres y Dorsales.
func totalDeElementos(_ contenedores: [any Contenedor]) -> Int {
// reduce(0): empieza en 0; $0 es lo acumulado y $1 el contenedor de esta vuelta.
contenedores.reduce(0) { $0 + $1.todos.count }
}
print(enMayusculas(["sol", "mar"])) // ["SOL", "MAR"]
print(totalDeElementos([Nombres(), Dorsales()])) // 5Intenta escribirlo tú, debajo de lider: nombres, que recibe con la etiqueta de cualquier secuencia de jugadores y devuelve sus nombres; y cuantosPuestosHay, que recibe sin etiqueta una lista de clasificaciones de tipos distintos y devuelve cuántos elementos suman entre todas.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// `some Sequence<Jugador>`: «algún tipo concreto que es una secuencia de Jugador».
public func nombres(de jugadores: some Sequence<Jugador>) -> [String] {
jugadores.map(\.nombre) // `\.nombre` es una ruta de propiedad: atajo de `{ $0.nombre }`
}
// `any Clasificacion`: cualquier clasificación; permite una lista de tipos distintos,
// a cambio de perder el tipo del `Elemento` y de un pequeño coste al usarla.
public func cuantosPuestosHay(_ clasificaciones: [any Clasificacion]) -> Int {
clasificaciones.reduce(0) { $0 + $1.ordenados.count }
}Otra pieza que se pega: los goleadores
Cuenta los goles de cada jugador (un diccionario «jugador → goles») y devuelve un Ranking<Jugador> con desempate por nombre. Usa piezas de las colecciones que se ven con calma en Colecciones y key paths, así que va hecho. Una frase de cada una:
flatMaprecorre una lista de listas y las junta en una sola:[["Ana", "Luis"], ["Ana"]].flatMap { $0 }da["Ana", "Luis", "Ana"]. Aquí junta los goleadores de todos los partidos.reduce(into:)recorre los elementos y va rellenando un valor que tú inicias:.reduce(into: [String: Int]())empieza con un diccionario vacío y, en cada vuelta, lo actualiza. Aquí cuenta cuántas veces sale cada jugador, es decir, sus goles.where Element == Partido: esta extensión solo existe cuando los elementos del array son exactamentePartido.[Jugador: Int]()es un diccionario vacío de jugador a número;acumulado[jugador, default: 0] += 1suma 1 a lo que hay para ese jugador, empezando en 0 si aún no está;goles.keysson sus claves, y[Jugador](goles.keys)las convierte en un array.
Pega el bloque al final de Ranking.swift.
El nombre goleadores aparece tres veces con sentidos distintos: el método de Partido (de Closures con listas) da la lista de jugadores que marcaron, y estos dos, el de [Partido] y el de Torneo, dan un Ranking<Jugador>.
// Solo para arrays de partidos: `[Partido]` también es `Array<Partido>`.
extension Array where Element == Partido {
public func goleadores() -> Ranking<Jugador> {
// `flatMap` junta en una sola lista los goleadores de todos los partidos.
// `reduce(into:)` va rellenando un diccionario «jugador → goles».
let goles = flatMap { $0.goleadores() }.reduce(into: [Jugador: Int]()) { acumulado, jugador in
// Si el jugador aún no está, parte de 0; cada gol suma 1.
acumulado[jugador, default: 0] += 1
}
return Ranking(
elementos: [Jugador](goles.keys), // los jugadores que marcaron (un array de Jugador)
puntos: { goles[$0] ?? 0 }, // sus goles
desempate: { $0.nombre < $1.nombre } // a igualdad de goles, por nombre
)
}
}
extension Torneo {
// El mismo ranking, sobre los partidos del torneo.
public func goleadores() -> Ranking<Jugador> { partidos.goleadores() }
}Probarlo en main.swift
El bloque va entre la línea de comentario // Genéricos: … y let porLongitud, en main.swift (Sources → torneo-cli). Recuerda que Equipo(…) es un init? (devuelve un opcional) y que el ! del final lo abre. Intenta escribirlo tú: crea el equipo Lobos, con Pedro y el dorsal 4, y un Toros contra Lobos con un gol de Iván para Toros en el minuto 20; saca los goleadores de ese partido y de alFinal, e imprime cada uno con su puesto, su etiqueta y sus goles; después, el líder y los nombres de todos. Lo que debes ver, delante de la línea del ranking de textos:
1. Ana (#9): 2
1. Iván (#7): 2
Líder: Ana; nombres: ["Ana", "Iván"]Intenta primero. Cuando termines, compara con la solución.Mostrar el código
let lobos = Equipo(nombre: "Lobos", plantilla: [Jugador(nombre: "Pedro", dorsal: 4)])!
let partidoDeToros = Partido(local: toros, visitante: lobos).registrando(.gol(minuto: 20, jugador: ivan, equipo: toros))
// Los goleadores de dos partidos, ya ordenados: Ana e Iván con 2 goles cada uno.
let tablaDeGoleadores = [alFinal, partidoDeToros].goleadores()
// Un Ranking se recorre con `for` porque es una Sequence.
for jugador in tablaDeGoleadores {
print("\(tablaDeGoleadores.puesto(de: jugador) ?? 0). \(jugador.etiqueta()): \(tablaDeGoleadores.puntosDe(jugador))")
}
print("Líder: \(lider(tablaDeGoleadores)?.nombre ?? "nadie"); nombres: \(nombres(de: tablaDeGoleadores))")Compila y ejecuta antes de copiar nada. Desde la Terminal, en ~/Marcador-Swift/Torneo:
xcrun swift build
xcrun swift run torneo-cliLos tests
RankingTests.swift son siete comprobaciones de Ranking con tipos distintos (uno con String, otro con Int, otro con jugadores), del mismo tipo que las anteriores, así que no los escribes: 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). El orden es siempre el mismo: crear el fichero, pulsar el botón, seleccionar todo, pegar.
- En el Project navigator, clic derecho sobre
Tests → TorneoTests→ New File from Template… → Swift File → Next →RankingTests→ Create. (Plan B:touch Tests/TorneoTests/RankingTests.swiftdesde~/Marcador-Swift/Torneo.) - Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
- Haz clic dentro de
RankingTests.swift, selecciona todo con Cmd+A y pega con Cmd+V.
Ahora pruébalos: xcrun swift test desde ~/Marcador-Swift/Torneo.
Un límite que verás. Ana e Iván tienen 2 goles cada uno, y los dos salen con el puesto 1. puesto(de:) mira los puntos y no el desempate: empatados en puntos, comparten puesto aunque uno vaya antes en la lista. Kotlin tiene el mismo límite.
Punto de control
xcrun swift build(desde~/Marcador-Swift/Torneo) compila sin errores.xcrun swift run torneo-cliimprime1. Ana (#9): 2y1. Iván (#7): 2, despuésLíder: Ana; nombres: ["Ana", "Iván"]yRanking de textos: ["Rayo FC", "Toros", "Lobos"]: el mismoRankingordena jugadores por goles y textos por longitud.xcrun swift testtermina conTest run with 58 tests in 8 suites passed: los 51 de la lección anterior más los 7 nuevos deRankingTests.- Sabes decir cuándo usarías
somey cuándoany.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.¿Qué es un
associatedtypeen un protocolo?2.Necesitas una lista con dos tipos distintos que cumplen
Contenedor. ¿Qué escribes?3.
func nombresOrdenados() -> some Contenedordevuelve siempreNombres(). ¿Qué ve quien llama?4.
Torneoguardalet reglamento: any Reglamentoy nosome Reglamento. ¿Por qué?