Inicialización en Swift

INICIALIZACIÓN en Swift en Español - Curso Swift | Tutorial Swift

Inicialización en Swift. Detrás de la inicialización de Value y Reference types hay más de lo que imaginas. Hoy vamos a ver qué pasa cuando se instancia una struct o clase. Hablamos de convenience y designated initializers

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇

La inicialización es el proceso de preparación de la instancia de una clase, struct o enum para su uso. Este proceso involucra dar un valor inicial a cada propiedad de la instancia y ejecutar otra preparación o inicialización que es requerida antes de que la nueva instancia esté lista para su uso.

Implementamos esta inicialización definiendo lo inicializadores, que son métodos especiales que pueden ser llamados para crear nuevas instancias de un tipo en particular.

Una diferencia respecto a Objective-C es que estos inicializadores no retornan ningún valor.

Instancias de una clase también pueden usar deinicializadores, sirven para ejecutar código antes de que la clase sea liberada en memoria.

Establecer valores iniciales a una propiedad

Las clases y structs deben establecer los valores de sus variables cuando se crea su instancia. Podemos establecer valores en las propiedades dentro de un inicializador, o dando valores por defecto.

Inicializadores

Como hemos dicho, llamamos a un inicializador para crear la instancia de un tipo. Para ello usamos la keyword init

init() {
	// perform some initialization here
}

Vamos a crear una struct User con una propiedad para almacenar el nombre del usuario:

struct User {
    var name: String
    
    init() {
        name = "SwiftBeta"
    }
}

var user = User()
print("The default user is \(user.name)")

// El resultado es "The default user is SwiftBeta"

Para crear la instancia de user hemos llamado a User() que es lo mismo que hacer User.init()

Propiedades con valores por defecto

En lugar de usar el método init() para dar valores a las propiedades de un tipo, podemos hacerlo directamente, asignando un valor por defecto a la propiedad:

struct Coworker {
    var name = "SwiftBeta"
}

let coworker = Coworker()
print("The defualt coworker is \(coworker.name)")

// El resultado es "The default coworker is SwiftBeta"

Personalizar la inicialización

Podemos personalizar la inicialización de una instancia añadiendo parámetros al init() o creando propiedades opcionales

Parámetros de inicialización

Podemos proporcionar parámetros de inicialización como parte de la definición de un inicializador, para definir los tipos y nombres de los valores a personalizar en el proceso de inicialización. Vamos a ver un ejemplo:

struct Database {
    var name: String
    
    init(withName name: String) {
        self.name = name
    }
}

let database = Database(withName: "mobile")

Hemos creado un inicializador que acepta como parámetro el nombre de la database.

Una nota importante, es que no podemos llamar al anterior inicializador sin usar las etiquetas de los argumentos (los nombres de los parámetros), es decir, no podríamos hacer lo siguiente:

let database = Database("mobile")

Ya que el resultado sería un error en tiempo de compilación. Pero tiene fácil solución, para poder ejecutarlo, bastaría con crear otro método inicializador de la siguiente manera:

 init(_ name: String) {
   self.name = name
 }
 
 let mobileDatabase = Database("mobile")

Propiedades de tipo opcional

Al crear instancias puede que tengamos propiedades que no tengan ningún valor en ese momento. El motivo es porque no es necesario tener un valor en tiempo de inicialización o porque en algún momento en el transurso de la lógica de esa instancia puede que esa propiedad no tenga valor (y por lo tanto sea nil).

Es por eso que podemos crear instancias sin haber dado un valor a las propiedades de tipo opcional. Por defecto, su valor es nil al inicializar.


Delegación de inicializadores para Value Types

Los inicializadores pueden llamar a otros inicializadores que se encarguen de crear la instancia. Este proceso es llamado delegación de inicializador, con esto impedimos crear código duplicado en varios inicializadores.

Las reglas para que la delegación de inicialización funcione varian para los value types (structs y enums). Al no soportar herencia, su delegación de inicialización es simple, porque solo pueden delegar la inicialización a otro inicializador que hayan definido ellos mismos.

Sin embargo, las clases pueden heredar de otras clases, esto significa que las clases tienen una responsabilidad adicional para asegurar que todas las propiedades de las que heredan tienen un valor asignado cuando se inicializa la instancia de la clase.

Con los value types, podemos usar self.init para referirnos a otros inicializadores del mismo tipo.

Sólo podemos usar self.init desde dentro de un inicializador

Ten en cuenta, que al crear un inicializador custom para un value type dejarás de tener el inicializador por defecto (o inicializador memberwise en el caso de las structs)

struct City {
    let name: String
    let countries: [String]
    
    init(name: String, countries: [String]) {
        self.name = name
        self.countries = countries
    }
    
    init(name: String) {
        self.init(name: name, countries: [])
    }
}

let city = City(name: "New York")

En el código anterior hemos creado 2 inicializadores, pero llamamos al que le pasamos el nombre de la ciudad y dentro de él llamamos al otro inicializador que nos dará un array vacío en countries.


Herencia e Inicialización de clase

Todas las propiedades (stored properties) de una clase deben tener un valor inicial cuando la clase se instancia.

En Swift tenemos 2 tipos de inicialización para las clases, estos inicializadores nos ayudan a dar un valor a las propiedades. Son conocidos como designated initializers y convenience initializers

Designated Initializers y Convenience Initializers

Los designated Initializers son los inicializadores principales de una clase. Estos inicializadores inicializan los valores de las propiedades y llaman al inicializador de la superclase para continuar con el proceso de inicialización.

Las clases tienden a tener varios designated initializers y es bastante común tener solo uno en una clase. Toda clase debe tener al menos uno. Son puntos de embudo a través de los cuales tiene lugar la inicialización y a través de los cuales el proceso de inicialización continúa subiendo por la cadena de la superclase.

Los convenience initializers son secundarios. Podemos definir un convenience initializer para que llame a un designated initializer desde la misma clase. Podemos usar los convenience initializers para crear instancias de una clase con valores específicos.

Creamos convenience initializers en una clase como atajos para inicializar una clase, para que no sea nos ahorre tiempo y sea más claro.

Sintaxis para designated initializers y convenience initializers

Designated initializers

init(parameters) {
	statements
}

Convenience initializers

convenience init(parameters) {
	statements
}

Delegación de inicialización para clases

Para simplificar la relación entre designated y convenience initializers, Swift aplica las siguientes 3 reglas para delegar las llamadas entre inicializadores:

Regla 1
Un designated initializer debe llamar a un designated initializer de su superclase.

Regla 2
Un convenience initializer debe llamar a otro inicializador de la misma clase.

Regla 3
Un convenience initializer debe finalmente llamar a un designated initializer.

Es decir:

  • Los designated initializers deben delegar hacía arriba.
  • Los convenience initializers deben delegar a través de la misma clase.

Aquí vemos como la superclase tiene 1 designated initializer y 2 convenience initializers. Un convenience initializer llama a otro convenicence initializer y finalmente éste llama al designated initializer para instanciar la clase.

La subclase tiene 2 designated initializers y un convenience initializer. El único convenience initializer debe llamar a uno de los 2 designated initializers que a la vez llaman al designated initializer de la superclase. (Y finalmente se crearía la instancia)

Ejemplos

Vamos a ver un ejemplo creando una clase base llamada Food con dos inicializadores: designated y convenience

class Food {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

let food = Food()
let foodWithName = Food(name: "Bacon")

Podemos utilizar el convenience initializer para crear una instancia de Food.

Las clases no tienen memberwise initializer, por eso hemos creado el designated initializer que tiene como parametro name.

Ahora vamos a crear una subclase de Food, la vamos a llamar RecipeIngredient

class RecipeIngredient: Food {
    var quantity: Int
   
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

Aquí tenemos un designated initializer, que nos sirve para dar valor a quantity en las instancias de RecipeIngredient. Al dar el valor de quantity delega la creación de la instancia a la superclase.

Si te fijas, el convenience initializer tiene la misma firma que uno de la superclase, es por eso que en este caso hemos puesto la keyword override.

Otro detalle interesante es, ¿cómo puede ser que cree una instancia de  RecipeIngredient haciendo RecipeIngredient()? muy sencillo, RecipeIngrediente hereda también los inicializadores de la superclase, es por eso que está disponible un init sin parámetros, y que a la vez delega la instancia al convenience initializer de RecipeIngredient.

Con la siguiente imagen queda más claro:

Los inicializadores pueden fallar

Hay casos en que queremos que un inicializador falle, es decir, al crear la instancia de un tipo queremos que si no cumple ciertos requisitos retorne nil.

El ejemplo que verás a continuación retorna nil si el valor que recibe es un entero mayor que 2 ya que no es un posible estado válido para el tipo Message:

enum Message: Int {
    case sent = 0
    case received
    case read
    
    init?(rawValue: Int) {
        switch rawValue {
        case 0:
            self = .sent
        case 1:
            self = .received
        case 2:
            self = .read
        default:
            return nil
        }
    }
}