La primera vez que ves los corchetes angulares <T> en código Swift, te detienes. Una letra mayúscula aparece junto al nombre de una función y, al abrir la documentación, te espera una firma como func map<T>(_ transform: (Element) -> T) -> [T]. Los genéricos son la puerta de entrada al nivel intermedio de Swift. Casi todo en la biblioteca estándar (incluidos Array, Dictionary y Optional) está hecho con genéricos; solo al superar esta barrera empiezas a leer su código.
Esta es la segunda entrega de la serie intermedia. Resume qué problemas resuelven los genéricos, cuándo hacen falta las restricciones y las cláusulas where, y cómo afectan al rendimiento.
El problema que resuelven los genéricos — rechazar la elección entre duplicación y seguridad de tipos
El problema se ve enseguida al intentar crear una función que intercambie dos valores en un mundo sin genéricos.
Una versión para Int no sirve para String. Copiarla para cada tipo multiplica el código. La alternativa parece ser una caja que acepte cualquier tipo: Any en Swift. Pero Any elimina la información de tipos. Hay que hacer casting al extraer valores, y un error como guardar un Int y extraer un String supera la compilación para terminar en un crash en tiempo de ejecución.
En resumen, solo quedaban Any, sin duplicación pero arriesgado, o copias por tipo, seguras pero duplicadas. Los genéricos rechazan esta elección.
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
<T> declara un espacio que se rellenará después con un tipo. Al llamar, T queda fijado a un tipo concreto y el compilador comprueba todo usando ese tipo. Una llamada incoherente como swapValues(&intA, &strB) produce un error de compilación. Hay una sola copia del código, pero la comprobación de tipos se hace para cada tipo: esa es la esencia de los genéricos.
Lo mismo se aplica a los tipos. Si declaras struct Stack<Element>, Stack<Int> y Stack<String> son tipos distintos, y el compilador impide insertar una cadena en una pila de Int. Array tiene exactamente esta estructura, y Optional, como vimos en la entrega sobre opcionales, era un enum genérico llamado enum Optional<Wrapped>. En realidad, ya usabas genéricos a diario.
Restricciones — de «cualquier tipo» a «un tipo con estas capacidades»
Por defecto, el espacio T acepta cualquier tipo. Pero eso también significa que no puede hacer nada: como el compilador no sabe nada de T, no puede compararlo, imprimirlo ni sumarlo.
func largest<T>(_ items: [T]) -> T? {
items.max(by: <) // error de compilación — Tno se garantiza que sea comparable
}
Aquí aparecen las restricciones. Al escribir <T: Comparable>, impones que T solo pueda ser un tipo que adopte Comparable; desde ese momento puedes usar < con T.
func largest<T: Comparable>(_ items: [T]) -> T? {
items.max()
}
Una restricción no es una pérdida, sino un intercambio. Reduces los tipos aceptados, pero aumentas lo que puedes hacer con ellos. La idea de combinar capacidades, tratada en la sección de protocolos, se encuentra aquí con los genéricos, porque las restricciones se expresan mediante protocolos.
Cuando las condiciones se complican, se usa una cláusula where. El significado es el mismo; where se vuelve obligatorio al imponer condiciones sobre tipos asociados de los parámetros de tipo.
// Elementsolo comparación entre colecciones cuyo Equatablees el mismo
func allEqual<C: Collection>(_ items: C) -> Bool where C.Element: Equatable {
guard let first = items.first else { return true }
return items.allSatisfy { $0 == first }
}
C.Element se refiere al tipo de elemento que contiene una colección: el tipo asociado (associated type). Lo veremos a fondo en una entrega posterior; por ahora basta entender que where es el lugar donde se impone esa condición.
En la práctica, conviene imponer solo las restricciones necesarias. Si Comparable basta, exigir también Hashable reduce los tipos utilizables. La lista exacta de restricciones debe reflejar las capacidades que usa realmente el cuerpo de la función.
Rendimiento — el compilador elimina el coste de la abstracción
A la pregunta «¿los genéricos no son lentos?» responde una optimización representativa de Swift: la especialización.
En principio, una función genérica desconoce el tipo recibido, así que debe transportar información de tipos en tiempo de ejecución y operar indirectamente. Pero si el compilador puede ver el punto de llamada, genera una versión específica para swapValues<Int>. Produce el mismo código máquina que escribir a mano la versión para Int. La abstracción genérica no implica pagar un coste en tiempo de ejecución: el compilador la elimina y crea código concreto. Es un caso emblemático de abstracción sin coste, como vimos en la primera entrega de filosofía.
La optimización funciona bien dentro del mismo módulo, pero se limita al cruzar fronteras de módulo (depende de cómo se distribuya la biblioteca). La conclusión práctica es que no hay razón para evitar genéricos por miedo al rendimiento; los cuellos de botella deben encontrarse midiendo. La optimización prematura ya tuvo su propio artículo.
Surge una pregunta natural: «Si recibo un tipo de protocolo (como items: [Comparable]), ¿en qué se diferencia de un genérico?». Es el tema de la próxima entrega. Los genéricos ofrecen polimorfismo estático, con el tipo fijado en compilación; los tipos de protocolo (existential) ofrecen polimorfismo dinámico, con tipos mezclados en tiempo de ejecución. También veremos por qué Swift distingue ambos mediante some y any.
Cuándo crear genéricos — criterios prácticos
Leer genéricos y diseñarlos son habilidades distintas, así que resumamos los criterios para crearlos.
es una señal cuando la misma lógica aparece por segunda vez cambiando solo el tipo. Empezar con genéricos pensando «algún día llegará otro tipo» suele ser sobrediseño. Aplica YAGNI (You Aren’t Gonna Need It — no lo construyas hasta necesitarlo): comienza con un tipo concreto y generaliza cuando aparezca duplicación real.
** encaja mejor con estructuras y algoritmos que con conceptos de dominio.** Cachés, respuestas paginadas y pilas, independientes de su contenido, son terreno natural para genéricos. Como APIResponse<User> y Cache<ImageKey, UIImage>. En cambio, convertir a la fuerza la lógica de un dominio concreto, como pagos de pedidos, solo complica la firma.
** es una señal para retroceder si la complejidad de la firma supera el beneficio para quien la usa.** Cuando aparecen tres o cuatro parámetros de tipo y tres líneas de where, conviene preguntarse si los compañeros podrán leer la llamada. El principio de Progressive Disclosure también aplica aquí: la declaración debe absorber la complejidad, no filtrarla hacia el uso.
Resumen
- Los genéricos rechazan la elección entre reutilización sin duplicación y seguridad de tipos. Hay una sola copia del código, pero la comprobación de tipos se realiza por tipo.
<T>declara un espacio de tipo; las restricciones (T: Comparabley where) estrechan ese espacio a cambio de ampliar las operaciones disponibles.- Gracias a la especialización, los genéricos suelen rendir como código concreto escrito a mano.
- Criterios: generaliza cuando aparezca la segunda duplicación, úsalo en estructuras y algoritmos, y detente si la complejidad de la firma supera el beneficio.
La próxima entrega trata la sintaxis hermana de los genéricos y una de las mayores fuentes de confusión del Swift actual: some y any. Exploraremos qué es some View y cuánto cuesta un existential.

![Imagen de portada de [Swift intermedio #2] Elimina duplicación y riesgos con genéricos](/assets/images/posts/7d69dc2a-8c58-4422-956e-940173ebaf99/1.jpg)