Saltar al contenido
KodigoSwift

Tutorial Swift – Código Genérico

Código Genérico

El día de hoy aprenderemos sobre los Genéricos en Swift. Con estos podemos escribir funciones y tipos mucho más flexibles y reusables que pueden trabajar a su vez con cualquier otro tipo, siempre sujetos a ciertos límites que nosotros definamos.

El código genérico constituye una de las características más potentes del lenguaje Swift, de hecho, un alto porcentaje de las librerías estándar de Swift están escrita con código genérico, incluso hasta nosotros hemos estado usando código genérico todo este tiempo, me refiero a los Array y Dictionary ya que ambos son colecciones genéricas…

¿Por qué?

Pues si analizamos con detenimiento, no existe un tipo ArrayInt o ArrayString, solamente contamos con una sola versión de esta colección. Por esta razón es que decimos que ha sido implementado usando código genérico, ya que nosotros podemos crear un Array que contenga valores de tipo Int y otro con valores String, de manera similar podemos crear un Dictionary con valores de cualquier tipo.

Motivación

Veamos un ejemplo clásico que motiva a su vez el uso de código genérico. Consiste en dos funciones NO genéricas llamadas swapInts y swapStrings:

…la salida en pantalla sería:

En este ejemplo tenemos dos funciones que nos vienen a ayudar con el intercambio de valores entre dos variables, para esto hemos usado parámetros inout (en el artículo sobre las funciones en Swift explicamos el uso de parámetros inout) y como se habrán dado cuenta hemos creado dos versiones, una enfocada en el tipo Int y la otra en String.

Ahora, imaginémonos que luego de unos pocos meses nuestro jefe nos informa que necesitamos un intercambio de valores para un tipo Double o para uno custom (personalizado), bajo estos requerimientos tendríamos que crear otra función similar a estas dos ya creadas, re-compilar nuestro código y volver a distribuir la nueva versión de nuestra aplicación.

¿Podemos reducir todo este trabajo? ¿Habrá alguna solución eficiente?

La respuesta es sí, veamos a continuación algunas soluciones posibles:

Usando Any

El lenguaje Swift provee dos alias de tipo para cuando trabajamos con tipos no específicos, es decir cuando no sabes que tipo de valor contendrá una variable, ellos son:

  • AnyObject – Puede representar una instancia de cualquier tipo de clase.
  • Any – Puede representar una instancia de cualquier tipo, incluyendo funciones.

Dicho esto pues una posible solución sería hacer uso de Any para unificar así el intercambio de valores en una sola función:

…la salida en pantalla es idéntica al ejemplo anterior.

Esta nueva versión del código comienza con la definición de una función llamada swapValues que es muy similar a las anteriores, se diferencia de estas solamente en el tipo de parámetros con los que trabaja, en este caso de tipo Any, permitiéndonos así pasar varios tipos de datos sin necesidad de duplicar o triplicar nuestro código.

¿Es esta una solución real?

No, solamente sería práctica en algunos casos, pero no en el que estamos desarrollando.

Swift no permite que pasemos una variable Int a una función que espera inout Any, por esto es que tuvimos que declarar nuestras variables como Any en lugar de Int y String. Es decir que no estamos interactuando con varios tipos de datos, hemos unificando nuestros valores numéricos y de cadena bajo el tipo Any para así poder pasarlo a la función swapValues.

El problema con este enfoque es que terminado el intercambio de valores si intentamos, como parte de nuestra lógica, pasar estos valores a otros métodos o funciones que quizás esperen un valor Int o String, pues no podremos, el compilador de Swift nos mostraría un error similar a este:

…el mensaje de error nos informa que no podemos convertir de un tipo Any al tipo del argumento esperado que en este caso es String. Es evidente que tener nuestros valores como de tipo Any nos limita un poco, lo ideal sería tener, de igual manera, una sola función pero que sea capaz de interactuar con cualquier tipo de datos sin necesidad de recurrir a Any.

Funciones genéricas

La solución real a este tipo de problemas es el uso de código genérico, lo que también se conoce como polimorfismo paramétrico y que no es más que la técnica donde una función es escrita de un modo en el que esta puede ejecutar sus operaciones sobre los valores de los parámetros sin importar de que tipo de dato sean estos.

Para lograr este enfoque tendríamos que modificar nuestra función de la siguiente manera:

…en la primera línea se concentran todos los cambios. Podemos notar el segmento <T> a continuación del nombre de la función y esto significa que los parámetros que estaremos manejando serán de tipo T. Al mismo tiempo todos sabemos que no existe el tipo T, y es que en este ámbito T viene a ser un comodín que enmascara cualquier tipo de dato. Así cuando trabajamos con enteros, T sería de tipo Int y cuando trabajamos con cadenas pues de tipo String, es decir T será del mismo tipo de dato que el de los parámetros que estemos pasando a la función. Como bien se explica en los comentarios de este ejemplo:

Es bueno aclarar que lo que le da esta denotación especial a la letra T es el hecho de haberla declarado dentro de <>, si hubiéramos escrito <A> por ejemplo, pues A sería el tipo genérico a manejar dentro de la función. Lo que sucede es que la mayoría de los libros siempre escogen T (deduzco que viene de Type), en otro lenguajes de programación también se usa T entonces como ven ya esto se ha vuelto como una norma a seguir.

Tipos genéricos

Al igual que podemos declarar funciones que interactúan con tipos de datos de manera genérica pues también podemos definir tipos genéricos.

Imaginemos la situación donde queremos definir nuestra propia colección de datos, una Pila. Recordemos que una Pila es una estructura de datos last-in first-out, es decir el último en entrar es el primero en salir, como en una pila de platos que ponemos sobre una mesa, el último que ponemos es el primero que podemos tomar de esta, luego seguiríamos con el de más abajo y así hasta llegar al último que está haciendo contacto con la mesa y que fue el primero en ponerse. Nuestra Pila tendrá, como todas, dos funciones básicas: la primera donde introduciremos (push) un elemento en ella y la segunda donde extraeremos (pop) él elemento que se encuentre en la cima de la misma, el último que se introdujo. Tal y como podemos ver en la siguiente imagen:

Comportamiento de una Pila

Si a la hora de implementar nuestra Pila usamos un enfoque tradicional en el cual no usamos código genérico, terminaríamos por tener una Pila que solamente tenga soporte para un solo tipo de dato, veamos:

…la salida en pantalla sería:

Luego de implementar nuestra pila creamos una instancia de la misma, luego hacemos uso del método push para introducir dos enteros y ya al final efectuamos tres llamadas al método pop el cual nos devuelve el último elemento en la pila. La tercera llamada al método pop fue deliberada para probar el bloque guard, como podemos observar nos retorna nil debido a que no hay un tercer elemento en la Pila.

Aunque este ejemplo funciona, sucede lo mismo que en el primer ejemplo de este artículo, está limitado a trabajar solamente con un tipo de dato. Así que veamos como podemos modificar este código para lograr una colección genérica:

…la salida en pantalla sería:

De la línea 26 a las 28 podemos constatar los beneficios de esta última versión, creamos tres pilas distintas de tres tipos de datos distintos, con una sola implementación, con el mismo código.

Funciones genéricas con varios comodines

Asumamos la necesidad de una función map, la cual ejecutará un closure (¿Qué es un Closure?) que el usuario defina sobre cada elemento de un arreglo, terminando por retornar otro arreglo con los resultados. La función en cuestión es la siguiente:

…vemos en la firma de esta función una variación en la sintaxis que habíamos usado hasta ahora, y es que tenemos dos comodines en lugar de uno, tenemos a T y U. Esto lo que viene a significar es lo mismo que hemos comentado hasta ahora con la diferencia de que en este método se manejarán dos tipos de datos distintos en lugar de uno, por ende T y U no pueden sustituir al mismo tipo de dato. Por ejemplo T puede ser Double y U quizás Int y viceversa, pero ambos jamás podrán ser del mismo tipo al mismo tiempo, es decir no pueden ser ambos Int, Double, String o cualquier otro tipo ya que de ser así no necesitaríamos a U, con T ya sería suficiente.

Como acabamos de comentar en esta función manejamos dos tipos de datos y lo expresamos de esa manera, luego se definen los dos parámetros que recibirá este método. El primero es un arreglo de tipo T y el segundo una función o closure que recibe un parámetro de tipo T y retorna un valor de tipo U, finalizamos con el valor de retorno de nuestra función que será un arreglo de tipo U.

Para darle un poco de contexto a estas variables diríamos que T es el tipo de dato de cada elemento en un arreglo. Luego U vendría a ser ese elemento T ya modificado por el closure, razón por la que necesitamos representarlo con otra letra cualquiera, para así, de manera explícita, informar al compilador que el tipo de dato no será el mismo.

En la primera línea creamos un arreglo vacío de tipo U que contendrá el arreglo final que retornaremos. Luego pasamos al bucle for que itera por cada elemento del arreglo de tipo T. Dentro del for hacemos uso del método append de la colección Array y sobre este método hacemos una llamada al closure, quedando almacenado el valor que este devuelva, al final retornamos el arreglo de tipo U.

Veamos un ejemplo de esta función en uso:

En la línea 15 creamos un arreglo de tipo String con tres cadenas, luego en la línea 17 creamos una constante llamada stringLengths que a su vez establecemos que sea igual al arreglo que retorne nuestra función map. Como podemos observar a map le pasamos el arreglo en su primer parámetro y acto seguido establecemos un closure que hace referencia a item que sería lo mismo que decir que $0 representa cada elemento del arreglo en cada iteración del bucle for. Sobre este elemento obtenemos la cantidad de caracteres en un valor de tipo Int y lo almacenamos en el arreglo result que al final es retornado y almacenado en stringLengths y que imprimimos mostrando la siguiente salida en pantalla:

…donde tenemos la cantidad de caracteres de cada palabra almacenada en el arreglo.

Métodos genéricos

Lo que hemos visto hasta ahora lo podemos hacer también con las clases y las enumeraciones, haciendo uso de la misma sintaxis, incluso los métodos de las clases o las estructuras pueden ser genéricos. Por ejemplo: nuestra recién creada función map la podemos convertir en un método de nuestra Pila, veamos:

…la salida en pantalla sería:

Como podemos constatar la función map ha sufrido algunos cambios para adaptarla a las necesidades de nuestra Pila. Como podemos ver en la línea 23 especificamos un solo comodín U cuando dentro del método trabajamos también con T. Lo que sucede es que T está siendo manejado a nivel de la estructura y lógicamente puede ser usado por nuestro método, por esta razón no tiene sentido especificarlo. En los parámetros ya no necesitamos el arreglo pues ya se ha especificado por la estructura así que solamente necesitamos especificar el closure que ejecutará sobre nuestro método el usuario de nuestra Pila, tal y como vemos en la línea 44, la cual también pudiéramos haber escrito de la siguiente forma:

Una nueva Pila es retornada con los resultados de multiplicar cada elemento por 2, valores que luego imprimimos en las líneas 51 y 52.

¿Es realmente map un método genérico?

Si nos estemos preguntando si el método map es realmente genérico por qué no podemos multiplicar una cadena de texto por un número determinado como en el último ejemplo, y esto al final nos limita a usar valores numéricos. Pues ciertamente es así, pero también tenemos que tener en cuenta de que hay una parte analítica que reside en nosotros, por esto jamás necesitaremos multiplicar una cadena de texto, pues no tiene sentido, por otra parte dado que el closure lo declaramos nosotros pues podríamos hacer lo siguiente:

…la salida en pantalla sería:

Así es, convertimos el texto a mayúsculas, algo que a su vez sería absurdo sobre valores numéricos, y lo hemos logrado sin modificar el código de nuestra Pila. Por esta razón decía que una parte del código genérico recae en nosotros, en el análisis de nuestro lado en cuanto a lo que tiene sentido y lo que no.

En este ejemplo que hemos abordado es más que evidente que no pudiéramos haber logrado el comportamiento deseado sin el closure ya que aquí es donde realmente se marca la diferencia, aquí es donde permitimos que el código del ejemplo se comporte de manera realmente genérica.

Esto no es todo acerca del tema, en un próximo artículo hablaremos sobre las restricciones por tipo en el código genérico, un apartado bien importante y que vendría a ser la segunda parte de este Tutorial Swift.

Falta aún mucho por aprender en nuestro camino a convertirnos en iOS Developer. Suscríbete a nuestra lista de correo mediante el formulario en el panel derecho y síguenos en nuestras redes sociales. Mantente así al tanto de todas nuestras publicaciones futuras.

Espero que todo cuanto se ha dicho aquí, de una forma u otra le haya servido de aprendizaje, de referencia, que haya valido su preciado tiempo.

Este artículo, al igual que el resto, será revisado con cierta frecuencia en pos de mantener un contenido de calidad y actualizado.

Cualquier sugerencia, ya sea errores a corregir, información o ejemplos a añadir será, más que bienvenida, necesaria!

RECIBE CONTENIDO SIMILAR EN TU CORREO

RECIBE CONTENIDO SIMILAR EN TU CORREO

Suscríbete a nuestra lista de correo y mantente actualizado con las nuevas publicaciones.

Se ha suscrito correctamente!