Los genéricos de Swift permiten escribir código flexible y reutilizable sin sacrificar la seguridad de tipos. La mayoría de desarrolladores usan genéricos básicos: funciones con <T>, protocolos con associatedtype, y la diferencia entre Array<Int> y Array<String>. Pero el sistema de tipos de Swift tiene capas más profundas tipos opacos con some, existenciales con any, primary associated types y parameter packs que son la clave para entender cómo funcionan SwiftUI, Combine y las APIs modernas.
some: tipos opacos (opaque types)
Un tipo opaco (some Protocolo) significa "algún tipo específico que conforma a este protocolo, pero no te digo cuál". El tipo concreto existe y es conocido por el compilador, pero está oculto para el llamador:
// Sin some: devuelve el tipo concreto, exponiendo los detalles de implementación
func crearVista() -> VStack<TupleView<(Text, Button<Text>)>> {
VStack {
Text("Hola")
Button("Acción") { }
}
}
// Con some: devuelve "alguna View", el tipo concreto es un detalle interno
func crearVista() -> some View {
VStack {
Text("Hola")
Button("Acción") { }
}
}
La clave de some: el tipo concreto es fijo (siempre el mismo tipo, no uno distinto en cada llamada), pero está encapsulado. El compilador puede optimizar porque conoce el tipo real.
// ? Error: las dos ramas retornan tipos distintos
func vistaCondicional(condicion: Bool) -> some View {
if condicion {
return Text("Texto") // Text
} else {
return Image("foto") // Image tipo diferente!
}
}
// Solución: @ViewBuilder o AnyView
@ViewBuilder
func vistaCondicional(condicion: Bool) -> some View {
if condicion {
Text("Texto")
} else {
Image("foto")
}
}
any: existenciales (existential types)
Un existencial (any Protocolo) es una caja que puede contener cualquier valor de cualquier tipo que conforme al protocolo, sin que el tipo concreto sea fijo. Requiere Swift 5.7+ y el prefijo any explícito:
// any: el tipo puede ser distinto en cada llamada, en cada elemento del array
var formas: [any Forma] = []
formas.append(Circulo(radio: 5))
formas.append(Rectangulo(ancho: 10, alto: 3))
formas.append(Triangulo(base: 4, altura: 6))
for forma in formas {
print(forma.area()) // Dispatch dinámico (más lento)
}
Los existenciales tienen un coste en rendimiento (box de memoria para tipos grandes, dispatch dinámico), pero permiten heterogeneidad que some no puede expresar.
Cuándo usar some vs any
// Usa some cuando:
// - El tipo concreto es siempre el mismo en cada contexto de uso
// - Quieres rendimiento óptimo (sin boxing)
// - El tipo tiene Self requirements o associated types
func crearGenerador() -> some RandomNumberGenerator { ... }
// Usa any cuando:
// - Necesitas colecciones heterogéneas
// - El tipo varía en runtime
// - La pérdida de rendimiento es aceptable para la flexibilidad
var plugins: [any Plugin] = cargarPlugins()
Primary associated types (Swift 5.7)
Los primary associated types permiten usar protocolos con tipos asociados de forma más concisa, especificando el tipo asociado directamente en el uso:
// Definición del protocolo con primary associated type
protocol Coleccion<Elemento>: Sequence where Element == Elemento {
associatedtype Elemento
func anadir(_ elemento: Elemento)
}
// Uso en lugar de escribir una restricción where compleja:
func procesarEnteros<C: Coleccion<Int>>(_ coleccion: C) { ... }
// O con some/any:
func procesar(_ coleccion: some Coleccion<String>) { ... }
var colecciones: [any Coleccion<Usuario>] = []
La librería estándar ya adoptó este patrón: Collection<Int>, AsyncSequence<String, Error>, Sequence<Double>.
Parameter packs (Swift 5.9): variádicos genéricos
Los parameter packs resuelven el problema de funciones que operan sobre un número arbitrario de tipos distintos. El ejemplo más claro es zip sobre múltiples secuencias:
// Sin parameter packs, necesitas versiones por aridad:
func zip<A, B>(_ a: A, _ b: B) -> (A, B) { (a, b) }
func zip<A, B, C>(_ a: A, _ b: B, _ c: C) -> (A, B, C) { (a, b, c) }
// ... y así hasta zip7, zip8...
// Con parameter packs (Swift 5.9):
func zip<each T>(_ valores: repeat each T) -> (repeat each T) {
return (repeat each valores)
}
let (a, b) = zip(1, "hola")
let (x, y, z) = zip(true, 3.14, "swift")
let (p, q, r, s) = zip(1, 2, 3, 4)
La sintaxis each T introduce un pack de tipos, y repeat each T expande el pack.
Ejemplo comparativo: los cuatro enfoques
protocol Transformable {
associatedtype Resultado
func transformar() -> Resultado
}
// 1. Genérico clásico: T debe ser conocido en compilación
func aplicar<T: Transformable>(_ valor: T) -> T.Resultado {
valor.transformar()
}
// 2. some: tipo opaco más expresivo en retornos
func crearTransformador() -> some Transformable {
MiTransformador()
}
// 3. any: existencial permite heterogeneidad
func procesarLote(_ items: [any Transformable]) {
for item in items {
_ = item.transformar() // Resultado es 'any Transformable.Resultado' pierde info de tipo
}
}
// 4. Parameter pack: opera sobre tipos múltiples en paralelo
func transformarTodos<each T: Transformable>(_ valores: repeat each T) -> (repeat (each T).Resultado) {
return (repeat (each valores).transformar())
}
Errores comunes al empezar con some y any
// Error 1: confundir some con genéricos en parámetros
func procesar(_ forma: some Forma) { ... } // OK any forma concreta
func procesar<F: Forma>(_ forma: F) { ... } // Equivalente, más explícito
// Error 2: intentar usar any donde el protocolo tiene Self requirements
protocol Comparable { func menor(que otro: Self) -> Bool }
var items: [any Comparable] = [] // ? Error no se puede usar existencial con Self
// Error 3: olvidar que some en retorno no permite tipos distintos por rama
func elegir(condicion: Bool) -> some Coleccion {
if condicion { return [1, 2, 3] } // Array<Int>
else { return ["a", "b"] } // Array<String> ? tipo distinto
}
Resumen
El sistema de genéricos de Swift ha evolucionado de los clásicos <T: Protocolo> hacia un modelo más expresivo donde some oculta el tipo concreto manteniendo su identidad (y el rendimiento), any permite heterogeneidad con coste en dispatch, los primary associated types hacen los protocolos más composables y los parameter packs eliminan la repetición por aridad. Entender cuándo usar cada uno es lo que separa las APIs bien diseñadas de las que exponen demasiado o sacrifican rendimiento innecesariamente.
