Qué son los Generics en Swift y para qué sirven
\n\nImagina que escribes una función que intercambia dos números. Luego necesitas la misma función para cadenas. Y después para fechas. Si no existieran los Generics (genéricos), tendrías que escribir tres funciones idénticas, diferenciándose solo en los tipos de datos. Esto viola el principio DRY (Don't Repeat Yourself) y hace que el código sea voluminoso.
\n\nGenerics (genéricos o tipos genéricos) es un potente mecanismo en Swift que permite escribir código flexible y reutilizable que funciona con cualquier tipo de dato. En lugar de especificar un tipo concreto, usas un placeholder (marcador de posición) que se reemplaza por el tipo real al llamar a la función o crear la estructura.
\n\nEn este artículo, analizaremos cómo funcionan los Generics en Swift y mostraremos con ejemplos prácticos cómo usarlos en el desarrollo de iOS.
\n\n1. Fundamentos de Generics: funciones y tipos
\n\nComencemos con un ejemplo simple. Escribamos una función que intercambie dos valores:
\n\n// Sin Generics — hay que escribir para cada tipo por separado\nfunc swapInts(_ a: inout Int, _ b: inout Int) {\n let temp = a\n a = b\n b = temp\n}\n\nfunc swapStrings(_ a: inout String, _ b: inout String) {\n let temp = a\n a = b\n b = temp\n}\n\n// Con Generics — una función para todos los tipos\nfunc swapValues<T>(_ a: inout T, _ b: inout T) {\n let temp = a\n a = b\n b = temp\n}\n\nvar x = 10, y = 20\nswapValues(&x, &y) // Funciona con Int\nprint("x: \\(x), y: \\(y)") // x: 20, y: 10\n\nvar str1 = "Hello", str2 = "World"\nswapValues(&str1, &str2) // Funciona con String\nprint("str1: \\(str1), str2: \\(str2)") // str1: World, str2: Hello\n\nAquí <T> es un parámetro de tipo universal. La letra T es solo una convención (de la palabra Type), puedes usar cualquier nombre: <Element>, <ValueType>, etc.
Los Generics no solo funcionan con funciones, sino también con tipos. Consideremos una estructura contenedora simple:
\n\n// Estructura genérica para almacenar un solo valor\nstruct Box<T> {\n let value: T\n \n init(_ value: T) {\n self.value = value\n }\n}\n\nlet intBox = Box(42) // Tipo Box<Int>\nlet stringBox = Box("Swift") // Tipo Box<String>\nlet arrayBox = Box([1, 2, 3]) // Tipo Box<[Int]>\n\nprint(intBox.value) // 42\nprint(stringBox.value) // Swift\n\nEl compilador de Swift determina automáticamente el tipo T basándose en el valor pasado. Esto se llama type inference (inferencia de tipos).
2. Restricciones de tipo (Type Constraints)
\n\nA veces es necesario restringir qué tipos pueden usarse con tu genérico. Por ejemplo, quieres escribir una función que encuentre el valor máximo en un array. Pero el operador > no funciona con todos los tipos, solo con aquellos que conforman (conform) el protocolo Comparable.
// Restricción: T debe conformar el protocolo Comparable\nfunc findMax<T: Comparable>(in array: [T]) -> T? {\n guard !array.isEmpty else { return nil }\n \n var maxValue = array[0]\n for element in array {\n if element > maxValue {\n maxValue = element\n }\n }\n return maxValue\n}\n\nlet numbers = [3, 7, 1, 9, 5]\nprint(findMax(in: numbers) ?? "No max") // 9\n\nlet words = ["apple", "zebra", "banana"]\nprint(findMax(in: words) ?? "No max") // "zebra" (por orden alfabético)\n\nLa sintaxis <T: Comparable> significa: "El tipo T debe conformar el protocolo Comparable". Se pueden agregar múltiples restricciones separadas por coma o usar where para condiciones más complejas:
// Múltiples restricciones con where\nfunc process<T>(_ value: T) where T: Numeric, T: Comparable {\n if value > 0 {\n print("Positivo