La genericidad es una de las características más poderosa de Swift, como también lo es en otros lenguajes de programación; nos permite definir estructuras de datos de tipo seguro (type-safe) y funciones sin tener que comprometernos con un tipo de dato en específico, aunque podemos definir algunas reglas.
Este tipo de funcionalidad nos permite reutilizar código, sobre todo, algoritmos relacionados al procesamiento de datos.
¿Para qué la genericidad?
Vamos a ver con un ejemplo, qué tipo de problemas la genericidad nos ayuda a resolver.
Supongamos que queremos desarrollar una estructura de datos Pila (Stack) que podamos utilizar con cualquier tipo de datos, la cual va a tener los métodos clásicos, poner un elemento en la pila (push) y extraer un elemento de la pila (pop). Vamos a asumir que Swift no soporta genericidad.
Lo ideal en este caso sería implementar la pila utilizando una estructura o clase base universal, como lo es por ejemplo object en C#, pero en Swift nos complicamos un poco, pues no existe este concepto, por lo tanto, tendríamos que crear la nuestra.
Así quedaría una posible implementación de una pila:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 1 | |
protocol Base { | |
} | |
// 2 | |
struct A : Base { | |
func mensajeA() -> String { | |
return "Soy de tipo A" | |
} | |
} | |
// 3 | |
struct B : Base { | |
func mensajeB() -> String { | |
return "Soy de tipo B" | |
} | |
} | |
// 4 | |
struct Pila { | |
// 5 | |
private var elementos = [Base]() | |
// 6 | |
mutating func poner(_ elemento: Base) { | |
elementos.append(elemento) | |
} | |
// 7 | |
mutating func extraer() -> Base { | |
return elementos.removeLast() | |
} | |
} | |
// 8 | |
var pilaA = Pila() | |
var a1 = A() | |
pilaA.poner(a1) | |
print((pilaA.extraer() as! A).mensajeA()) | |
// 9 | |
var pilaB = Pila() | |
var b1 = B() | |
pilaB.poner(b1) | |
print((pilaB.extraer() as! B).mensajeB()) |
- Definimos el protocolo Base que vamos a utilizar como tipo base.
- Definimos una estructura A que hereda del tipo Base, en este caso, como Base es un protocolo, no sería técnicamente heredar sino adoptar o conformar, e implementa un método llamado mensajeA() que devuelve la cadena “Soy de tipo A”.
- Definimos una estructura B que adopta o conforma el protocolo Base e implementa un método llamado mensajeB() que devuelve la cadena “Soy de tipo B”.
- Definimos la estructura Pila.
- Definimos un arreglo llamado elementos donde van a guardarse los elementos de la pila, este arreglo lo declaramos de tipo Base para poder utilizar la estructura Pila con cualquier tipo que adopte el protocolo Base (como las estructuras A y B).
- Implementamos un método llamado poner() para colocar elementos en la pila, el cual recibe un elemento de tipo Base.
- Implementamos un método llamado extraer() para extraer un elemento de la pila.
- Ejemplo de cómo utilizamos la estructura Pila para poner/extaer elementos de tipo A. En este caso vemos como luego de extraer el elemento hacemos un casting del elemento que extraemos a tipo A, pues el método extraer() lo devuelve como tipo Base, para luego poder ejecutar el método mensajeA().
- Ejemplo de cómo utilizamos la estructura Pila para poner/extaer elementos de tipo B. En este caso hacemos casting a tipo B al elemento que extraemos para poder ejecutar el método mensajeB().
El truco está en utilizar un tipo base en la implementación de la estructura Pila, de modo que podamos utilizar la misma con cualquier tipo que herede de o conforme este tipo base. Sin un tipo base, tendríamos que implementar varias versiones de la estructura Pila por cada tipo de datos que queramos utilizar, por ejemplo, una implementación de la Pila para tipos A, otra para tipos B y así sucesivamente, con lo cual estaríamos duplicando código.
De igual manera podemos aplicar lo que hemos visto hasta ahora a las funciones, si queremos reutilizar una función que implementa algun algoritmo aplicable a tipos de datos diferentes.
Los inconvenientes de este tipo de soluciones se ven a simple vista, por ejemplo, el uso de casting para poder utilizar los métodos definidos en los tipos A y B y la duplicación de código si no utilizamos (o podemos utilizar) un tipo base. También, si no lo habías notado, en pilaA podemos colocar elementos de tipo B y en pilaB colocar elementos de tipo A, sólo que fallaría en la parte del casting.
Nota: Si no tienes una MacBook/MacBook Pro etc. o alguna computadora con OSX y Xcode puedes probar en https://iswift.org/playground.
Genericidad al rescate
Es aquí cuando la genericidad viene a ayudarnos, veamos como la versión genérica de Pila:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 1 | |
struct A { | |
func mensajeA() -> String { | |
return "Soy de tipo A" | |
} | |
} | |
// 2 | |
struct B { | |
func mensajeB() -> String { | |
return "Soy de tipo B" | |
} | |
} | |
// 3 | |
struct Pila<T> { | |
// 4 | |
private var elementos = [T]() | |
// 5 | |
mutating func poner(_ elemento: T) { | |
elementos.append(elemento) | |
} | |
// 6 | |
mutating func extraer() -> T { | |
return elementos.removeLast() | |
} | |
} | |
// 7 | |
var pilaA = Pila<A>() | |
var a1 = A() | |
pilaA.poner(a1) | |
print(pilaA.extraer().mensajeA()) | |
// 8 | |
var pilaB = Pila<B>() | |
var b1 = B() | |
pilaB.poner(b1) | |
print(pilaB.extraer().mensajeB()) |
- Definimos una estructura A con el método llamado mensajeA() que devuelve la cadena “Soy de tipo A”. Lo único que cambiamos es que no hereda de ningún tipo o conforma algún protocolo.
- Definimos una estructura B con el método llamado mensajeB() que devuelve la cadena “Soy de tipo B”. Igualmente no hereda de ningún tipo o conforma algún protocolo.
- Definimos la estructura Pila, en esta ocasión vamos a especificar que Pila es un tipo genérico, esto quiere decir que puede trabajar con cualquier otro tipo de datos. En Swift podemos crear tipos genéricos a partir de clases, estructuras y enumeraciones. Para especificar con cuál o cuáles tipos nuestra estructura Pila va a trabajar hacemos uso de los parámetros de tipo (type parameters), estos no son más que marcadores que luego sustituimos con un tipo de datos cuando vayamos a utilizar nuestro tipo genérico. La forma de especificarlos es colocándolos separados por coma entre los signos , en el caso de Pila, sólo necesitamos uno y le llamaremos T, noten cómo lo definimos: luego del nombre de la estructura. Si hubiésemos necesitado más parámetros podríamos haberlos colocado separado por coma, como por ejemplo , pueden se nombrados como prefieran aunque es recomendado utilizar nomenclatura “upper camel case”, ejemplo “TipoDeElemento”.
- Definimos un arreglo llamado elementos donde van a guardarse los elementos de la pila, esta vez, el tipo de datos de los elementos del arreglo va a ser T, que es el parámetro de tipo que definimos para la estructura.
- Implementamos el método poner(), la diferencia está en que el elemento que recibimos va a ser de tipo T.
- Implementamos el método extraer(), en esta ocasión retorna un elemento de tipo T. Creo que ya nos hacemos una idea de cómo funciona la genericidad 🙂
- Aquí vemos cómo utilizar la estructura Pila con el tipo de datos A, a la hora de crear la instancia pilaA, especificamos que Pila va a trabajar con el tipo A a través del uso del parámetro de tipo: Pila. El compilador por debajo va a crear una versión de Pila para la cual va a reemplazar el marcador (parametro de tipo) T por el tipo A, de modo que el método poner() ahora sólo va a aceptar elementos del tipo A y extraer() va a devolver elementos de tipo A, por lo que no necesitamos hacer casting para acceder a los métodos del tipo A.
- Exactamente lo mismo que hicimos para la pilaA lo hacemos para la pilaB.
Un modo sencillo de verlo es, mirar la implementación de Pila como una plantilla para crear tipos, donde los parámetros de tipo, en este caso T, son marcadores que van a ser reemplazados por cualquier otro tipo de datos que utilicemos a la hora de crear una instancia de Pila.
De este modo eliminamos la necesidad de duplicar código o de crear tipos base.
En Swift también podemos escribir funciones genéricas, la forma en que podemos especificar los parámetros de tipo para un función es colocándolos luego del nombre de la función, por ejemplo: func intercambiarValores(_ a: inout T, _ b: inout T).