Saltar al contenido
KodigoSwift

Tutorial Swift – Inicializadores Designados y de Conveniencia

Inicializadores Designados y de Conveniencia

En este Tutorial Swift seguiremos aprendiendo sobre los inicializadores, en esta ocasión acerca de los inicializadores designados y de conveniencia, como siempre veremos la sintaxis, sus características y sentido de ser, junto a ejemplos de uso.

Un inicializador designado es aquel que funge como primario de una clase, que inicializa todas las propiedades y que llama al inicializador de una clase padre o superclass en caso necesario, esto último en pos de continuar el proceso de inicialización hacia arriba en la cadena de herencia. Cada clase debe tener aunque sea un inicializador designado algo que en ocaciones se logra heredando uno o más inicializadores designados a partir de nuestra clase padre.

Los inicializadores por conveniencia vienen a dar soporte a una clase, son inicializadores secundarios. Podemos declarar un inicializador de conveniencia que llame a otro designado de la misma clase y con los valores por defecto para los parámetros de este. Así también podemos usar un inicializador designado en pos de crear una instancia de una clase para un caso de uso determinado o algún valor de entrada específico.

Sintaxis

Los inicializadores designados no tienen misterio alguno:

…en el caso de los de conveniencia su sintaxis difiere levemente, haciendo uso de la palabra clave convenience para diferenciarlo del anterior:

Delegación de Inicializadores para Tipos de Clase

En pos de simplificar la relación entre los inicializadores designados y de conveniencia, Swift aplica las siguientes tres reglas para la delegación de llamadas entre inicializadores:

  • Regla 1: Un inicializador designado debe llamar a su homólogo de la clase padre o superclass.
  • Regla 2: Un inicializador de conveniencia debe llamar a otro inicializador de la misma clase.
  • Regla 3: Un inicializador de conveniencia debe, en última instancia, llamar a un inicializador designado.

Tal y como sugieren en la documentación oficial una manera de recordar esto sería:

Los inicializadores designados deben siempre delegar hacia arriba en la cadena de herencia, mientras que los inicializadores de conveniencia deben siempre delegar a través de los inicializadores de la misma clase.

La siguiente imagen ilustra esta reglas:

Delegacion de Inicializadores

En este diagrama Superclass o la clase padre, tiene un solo inicializador designado y dos de conveniencia. Un inicializador de conveniencia llama a otro de conveniencia, el cual por su parte hace una llamada al designado. Esto satisface las reglas 2 y 3 que listamos arriba. Por su parte la clase hija Subclass cuenta con dos inicializadores designados y uno de conveniencia, este último debe llamar a uno de los dos designados ya que solamente puede llamar a otro inicializador de la misma clase, acorde con las reglas 2 y 3. A su vez los dos inicializadores designados deben llamar al inicializador designado de la clase padre en pos de satisfacer la regla 1.

Continuamos con una imagen:

Delegacion de Inicializadores - Embudo

…donde podemos observar una jerarquía de clases más compleja donde los inicializadores designados actúan como un embudo hacia la inicialización, simplificando así la interrelación entre la cadena de clases.

Inicialización en Dos Fases

La inicialización de clases en Swift es un proceso en dos etapas. En la primera de ellas, a cada propiedad almacenada se le asigna un valor inicial. Una vez que el estado inicial de cada propiedad ha sido determinado, la segunda etapa o la segunda fase comienza, y es el momento donde tenemos la oportunidad de personalizar las propiedades almacenadas antes de que la instancia sea considerada lista para su uso.

Durante estas fases el compilador de Swift ejecuta cuatro chequeos de seguridad en pos de que la inicialización termine de manera satisfactoria:

  • Chequeo 1: Un inicializador designado debe asegurarse de que todas las propiedades introducidas por la clase han sido inicializadas antes que se delegue hacia la clase padre o superclass.
  • Chequeo 2: Un inicializador designado debe delegar hacia su clase padre antes de asignar un valor a una propiedad heredada. Si esto no se ejecuta en este orden el valor asignado por nuestro inicializador designado será sobre escrito por el inicializador de la clase padre.
  • Chequeo 3: Un inicializador de conveniencia debe delegar a otro inicializador antes de asignar un valor a una propiedad.
  • Chequeo 4: Un inicializador no puede llamar a ningún método de instancia, leer los valores de ninguna propiedad de instancia o hacer referencia a self como un valor hasta después que la primera fase de inicialización se haya completado.

Una instancia de clase no es válida hasta que la primera fase termina. Las propiedades puedes ser consultadas y los métodos pueden ser llamados, una vez que la instancia de la clase es establecida por el compilador como válida, como acabamos de decir, al final de esta primera fase.

Veamos en detalles los pasos que se desarrollan dentro de estas dos fases:

Fase 1

  • Un inicializador designado o de conveniencia es llamado.
  • La memoria para la nueva instancia es asignada aunque aún no se ha inicializado.
  • Un inicializador designado de esta clase confirma que todas las propiedades almacenadas tienen un valor. La memoria de estas propiedades ahora se encuentra inicializada.
  • El inicializador designado delega en el inicializador de su clase padre para que este ejecute la misma tarea para con sus propiedades almacenadas.
  • Esto continua hacia arriba en la cadena de herencia hasta que se llega a la cima.
  • Una vez que la cima de la cadena de herencia es alcanzada y la clase final de esta cadena de herencia se ha asegurado de que todas sus propiedades almacenadas tienen un valor, la memoria de la instancia que estamos creando se considera completamente inicializada y la fase 1 se ha completado.

Fase 2

  • Ahora en reversa, desde la cima de la cadena de herencia hacia abajo, cada inicializador tiene la opción de personalizar la instancia un poco más. Los inicializadores ahora sí pueden hacer referencia a self y pueden consultar y/o modificar los valores de las propiedades, llamar a los métodos de la instancia.
  • Por último, cualquier inicializador de conveniencia en la cadena tiene también la posibilidad de personalizar la instancia y trabajar con self.

En la imagen a continuación podemos observar como luciría la fase 1:

Inicializacion en Dos Fases - Fase Uno

En el ejemplo de esta imagen el proceso de inicialización comienza con la llamada al inicializador de conveniencia de la subclase. Este inicializador aún no pude modificar ninguna propiedad, delega hacia el inicializador designado de la misma clase.

El inicializador designado se asegura de que todas las propiedades de la subclase tengan un valor, para luego llamar al inicializador designado de la clase padre para continuar así la inicialización hacia arriba en la cadena de herencia.

En este punto y tan pronto como no hayan más propiedades por inicializar, la memoria de la clase padre ya se considera completamente inicializada y la fase 1 queda completada.

Aquí vemos como luce la fase 2 de este mismo ejemplo:

Inicializacion en Dos Fases - Fase Dos

Como inicio de esta fase nos encontramos ante la posibilidad de personalizar la instancia de ser necesario. Una vez que el inicializador designado ha finalizado pues su homólogo de la subclase puede también ejecutar tareas de personalización, y al terminar éste pues también lo podrá hacer el inicializador de conveniencia. Como podemos constatar en la medida que los inicializadores van terminando sus tareas el flujo de inicialización retorna nuevamente a la llamada inicial.

Inicializadores Designados y de Conveniencia en acción

El siguiente ejemplo muestra a los inicializadores designados, los inicializadores de conveniencia y a la herencia de inicializadores automática en acción. Este ejemplo define una jerarquía de tres clases llamadas Food, RecipeIngredient y ShoppingListItem con las cuales vamos a demostrar todo cuanto hemos comentado hasta el momento.

La clase base en esta jerarquía es Food, la cual es una clase bastante sencilla donde encapsulamos el nombre de productos alimenticios. Esta clase introduce o declara una sola propiedad de tipo String llamada name y también provee dos inicializadores para la creación de instancias de esta clase:

La siguiente imagen muestra la cadena de inicialización para la clase Food:

Inicializadores Designados y de Conveniencia - Clase Food

Las clases no cuentan con un inicializador de miembro por miembro, razón por la que hemos incluido un inicializador designado que toma un solo argumento llamado name y es nuestra vía para crear una instancia de Food con un nombre asociado a esta:

Decimos que init(name: String) es un inicializador designado ya que en este nos aseguramos de que todas las propiedades de la clase sean inicializadas. Como podemos observar la clase Food no cuenta con una clase padre por lo que no es necesario delegar en esta y por ende no tendría sentido ejecutar super.init().

La clase Food también provee un inicializador por conveniencia init() sin argumentos, el cual establece un nombre por defecto delegando hacia el inicializador designado y que evidentemente viene a ser usado en ocaciones donde no especificamos un nombre:

La segunda clase en la jerarquía es una subclase de Food llamada RecipieIngredient y que viene a modelar un ingrediente dentro de una receta de cocina. Esta clase está conformada por una propiedad de tipo Int llamada quantity junto a otra de nombre name que es heredada de Food, también se definen dos inicializadores:

En la siguiente imagen se muestra la cadena de inicialización para la clase RecipieIngredient:

Inicializadores Designados y de Conveniencia - Clase RecipieIngredient

La clase RecipieIngredient contiene un inicializador designado init (name: String, quantity: Int) que contiene todos los argumentos para una instancia completamente funcional. Este inicializador comienza por asignar el argumento quantity a la propiedad del mismo nombre y luego de esto delega hacia el inicializador designado de la clase padre Food el cual se encargará de inicializar la propiedad name, al mismo tiempo que satisfacemos el chequeo de seguridad 1 del cual hablamos en la inicialización en dos fases.

En el caso del inicializador por conveniencia init(name: String) sobre-escribe al inicializador designado con la misma firma de la clase padre y como tal debe ser declarado con la palabra clave override. Nuestro inicializador de conveniencia cuenta solo con un argumento y nos crea una instancia a partir de los datos introducidos, asumiendo 1 para la propiedad quantity, datos que luego delega al inicializador designado de la misma clase.

A pesar de que RecipieIngredient proporciona el inicializador de conveniencia init(name: String), esta clase ha declarado, no obstante, todos los inicializadores designados de la clase padre. Por lo tanto RecipieIngredient automáticamente hereda todos los inicializadores de conveniencia de su clase padre Food.

En este ejemplo, la clase padre de RecipieIngredient es Food, la cual tiene un solo inicializador de conveniencia init(),  por lo tanto, este inicializador es heredado por RecipieIngredient. La versión heredada de init() es exactamente la misma versión de Food, con la excepción de que esta delega hacia la versión de init(name: String) en la clase RecipieIngredient en lugar de Food.

La tercera clase y final dentro de la jerarquía es una subclase de RecipieIngredient llamada ShoppingListItem y que viene a modelar un ingrediente de una receta pero en una lista de compras.

Cada elemento en la lista de compras comienza como “unpurchased”. Para representar este hecho ShoppingListItem introduce una propiedad bo0leana llamada purchased, con un valor por defecto de false.  Esta clase también declara una propiedad computada llamada description y que provee de un texto descriptivo relativo a la instancia:

Como esta clase provee valores por defecto para todas sus propiedades y no define ningún inicializador, pues automáticamente hereda todos los inicializadores designados y de conveniencia de su clase padre.

En la siguiente imagen podemos observar la cadena de inicialización para estas tres clases:

Inicializadores Designados y de Conveniencia - Clase ShoppingListItem

Veamos un ejemplo donde hacemos uso de los tres inicializadores heredados:

…la salida en pantalla sería:

Aquí tenemos un arreglo llamado breakfastList el cual almacena tres instancias de ShoppingListItem, arreglo que es inferido por el compilador de Swift como de tipo [ShoppingListItem]. Una vez creado este arreglo, el nombre del artículo en la lista de compra cambia de “[Unnamed]” a “Orange juice” y se marca como comprado. Al final podemos comprobar que los cambios se han aplicado correctamente al imprimir la descripción de cada elemento de nuestro arreglo.

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!