Tutorial Swift - Genéricos en Swift (Aprende a programar apps para iOS)
Tutorial Swift - Genéricos en Swift (Aprende a programar apps para iOS)

GENÉRICOS en Swift en Español - Curso Swift | Tutorial Swift

Genéricos en Swift (o Generics en Swift) nos permiten crear código que funciona con distintos tipos. Es decir, podemos preparar nuestro código para que funcione con Int, String, etc. Muy útil para evitar duplicaciones y crear un código más elegante.

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
Aprende Swift desde cero

Hoy en SwiftBeta vamos a ver los Genéticos en Swift. El código genérico nos permite escribir código más flexible, funciones reusables y los tipos que creamos pueden trabajar con cualquier tipo (String, Int, etc). Hoy veremos algunos ejemplos.

Es abstraer tu código para que funcione con cualquier tipo.

Los genéricos es una de las funcionalidades más potentes de Swift. Y está muy extendido en la librería Standar de Swift. Un ejemplo muy claro es el tipo Array y el Dictionary, los dos son colecciones genéricas. Podemos crear un Array que almacena Ints o otro Array que almacena Strings (lo mismo pasa con los Dictionary).

¿Qué problemas resuelven los genéricos?

Imagínate que tienes que crear una función para poder intercambiar el valor entre dos variables de tipo String. Lo que hay en la variable A se asigna a la variable B y lo que hay en la variable B se asigna a la variable A. Harías algo parecido a:

func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let tempA = a
    a = b
    b = tempA
}

var valueA = "AA"
var valueB = "BB"

swapTwoStrings(&valueA, &valueB)

print("valueA A is \(valueA), and valueB B is \(valueB)")

// El resultado es: "valueA A is BB, and valueB B is AA"
Función que intercambia dos valores de Strings

Pero, qué pasa si queremos hacer lo mismo para otros tipos? para Int, Double, etc. Crearíamos el siguiente código:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let tempA = a
    a = b
    b = tempA
}


func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let tempA = a
    a = b
    b = tempA
}
Función que intercambia dos valores de Int y Double

Como bien has observado, el body de estas tres funciones es idéntico. Pero tenemos que crear tres funciones distintas para cada tipo. Si usamos los genéricos podemos crear una única función.


Funciones Genéricas en Swift

Ahora vamos a crear una función que permita usarse con cualquier tipo:

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let tempA = a
    a = b
    b = tempA
}
Función Genérica para intercambiar dos valores

El body de la función es exactamente igual a las funciones que hemos creado más arriba. Lo que cambia es en la firma de la función.

func swapTwoStrings(_ a: inout String, _ b: inout String)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)
Firmas de una funcion de tipo String y la otra genérica

Al crear la función genérica hemos añadido <T> después del nombre de la función y en cada parámetro hemos especificado que será de tipo T. Al hacer esto, usamos un placeholder en lugar de especificar un tipo concreto.

Es una forma de decir a la función que acepte cualquier tipo y aplique la lógica que hay dentro del body

Para usar la función genérica lo hacemos como siempre. No tiene ningún trato especial:

var myName = "SwiftBeta"
var myBrand = "Aprende a programar en Swift"

swapTwoValues(&myName, &myBrand)

var firstDayOfTheMonth = 1
var lastDayOfTheMonth = 30

swapTwoValues(&firstDayOfTheMonth, &lastDayOfTheMonth)

Nombres a los tipo de parámetros en Swift

En muchos casos los tipos de los parámetros tienen nombres descriptivos, como Key y Value en los Dictionary<Key, Value> o Element en los Array<Element>. Así cuando lees Element, sabes que está relacionado con los elementos que se añaden dentro del Array.

Hay otras ocasiones en que podemos dar un nombre sencillo, para ello, podemos dar valores como el que hemos visto en el anterior ejemplo, como T, U, V, etc.


Tipos Genéricos en Swift

A parte de tener funciones genéricas podemos tener tipos genéricos. Son clases, struct y enums que pueden trababjar con cualquier tipo (igual que los Array o Dictionary).

Vamos a ver un ejemplo creando un Stack, FIFO (First In First Out) donde el primero en entrar es el primero en salir. En un Stack puedes realizar dos operaciones:

  • Push: Ir añadiendo elementos, siempre uno encima del otro.
  • Pop: Y también puedes sacar el último elemento que has añadido .

Vamos a ver como harías un Stack de Strings:

struct StackOfStrings {
    var items = [String]()
    
    mutating func push(_ item: String) {
        items.append(item)
    }
    
    mutating func pop() -> String {
        return items.removeLast()
    }
}

var stackOfString = StackOfStrings()
stackOfString.push("Swift")
stackOfString.push("Beta")
stackOfString.push("Aprender a Programar en Swift")

stackOfString.items

// El resultado es: ["Swift", "Beta", "Aprender a Programar en Swift"]

stackOfString.pop()
stackOfString.items

// El resultado es: ["Swift", "Beta"]

¿Sencillo verdad? ¿qué pasaría si quisieras utilizar lo mismo con Ints, Doubles, etc? Tendrías que crear StackOfInts, StackOfDoubles, etc. O podrías crear un stack que aceptara elementos genéricos.

struct Stack<Element> {
    var items = [Element]()
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

var stack = Stack<Int>()
stack.push(1)
stack.push(2)
stack.push(3)

stackOfString.items

stack.pop()

Restricciones de tipo

En los ejemplos anteriores no hemos puesto ninguna restricción al tipo. Cualquier tipo se puede usar en el Stack. Cuando ponemos restricciones en el tipo es porque queremos que hereden de una clase específica o que conformen un protocolo.

Los Dictionary en Swift tienen una restricción en sus tipos. No puedes usar cualquier tipo como key, toda key debe conformar el protocolo hashable. Es necesario para saber si una key ya contiene un valor. Sin este requerimiento, no podríamos saber si debemos insertar o reemplazar un valor para una key en particular, tampoco podríamos encontrar un valor dada una key.

La sintaxis es muy sencilla, vamos a verlo con el ejemplo de los Stacks:

struct StackOfViews<Element: UIView> {
    var items = [Element]()
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

En este ejemplo hemos puesto una única restricción, que todos los elementos hereden de UIView, si intentamos meter elementos de tipo Int, daría el siguiente error:

var stackOfViews = StackOfViews<Int>()
stackOfViews.push(1)
stackOfViews.push(2)

// Error
// 'StackOfViews' requires that 'Int' inherit from 'UIView'

Solo funciona si metemos en el Stack elementos que heredan de UIView:

var stackOfViews = StackOfViews<UIView>()
stackOfViews.push(UILabel())
stackOfViews.push(UIButton())
stackOfViews.push(UIView())

stackOfViews.items

// El resultado dentro del Stack es: [UILabel, UIButton, UIView]

Associated Types

Cuando definimos un protocolo, es aveces util declarar uno o dos associated types como parte de la definición del protocolo.

Los associated type se usan para crear protocolos genéricos

Un associated type proporciona un placeholder (Key, Value, Element, U, T, V, etc) a un tipo que se usa como parte del procotolo.

Ahora vamos a crear un protocolo con associated types que conformará nuestra Stack:

protocol Container {
    associatedtype Item
    var items: [Item] { get set }
    mutating func push(_ item: Item)
    mutating func pop() -> Item
}

Hemos creado un protocolo que tiene un associated type que puede ser de cualquier tipo. El protocolo tiene varios requerimientos, un array de items y dos métodos.

Conformar este protocolo es tan simple como:

struct StackAssociatedType<Element>: Container {
    typealias Item = Element
    var items = [Element]()
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element {
        items.removeLast()
    }
}

Como has podido ver, los genéricos ayudan a reducir código pero en algunas ocasiones hacen que sea más dificil de comprender. Hay que buscar el equilibrio.