DEV Community

David Goyes
David Goyes

Posted on

Swift #24: Protocolos

Un protocolo en Swift puede definir propiedades y métodos, a manera de plantilla, que las estructuras pueden compartir.

Los protocolos pueden tener extensiones, donde se puede escribir una implementación por defecto de algún método definido en el protocolo. Esta implementación puede ser usada por las estructuras, quienes también pueden sobrescribirla.

Sintaxis

Se puede declarar propiedades y métodos, pero no se les puede definir. Al definir una propiedad también se debe señalar si es solo de lectura (i.e. { get }) o de lectura-y-escritura (i.e. { get set }) - no es posible crear solo de escritura.

protocol NombreDelProtocolo {
  var propiedad1: Tipo1 { get set }
  func metodo1(param1: Tipo1, param2: Tipo2) -> Tipo3
}
Enter fullscreen mode Exit fullscreen mode

Una estructura puede conformar un protocolo, definiendo/implementando las propiedades y métodos declaradas en el protocolo, así:

struct MiEstructura: NombreDelProtocolo {
  var propiedad1: Tipo1
  var otraPropiedad2: Tipo2
  func metodo1(param1: Tipo1, param2: Tipo2) -> Tipo3 {
    ...
  }
  func otroMetodo2(param1: Tipo3) {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Protocolo como tipo de dato

Como los protocolos son tipos de datos, podemos usarlos para definir variables o valores de retorno de un método. Por ejemplo:

func getEntity(value: NombreDelProtocolo) -> NombreDelProtocolo {
  MiEstructura(propiedad1: ..., otraPropiedad2: ...)
}
Enter fullscreen mode Exit fullscreen mode

Protocolo genérico

Se debe definir el tipo de dato genérico dentro del protocolo con un associatedtype.

protocol NombreDelProcolo {
  associatedtype TipoPlantilla
  var propiedad: TipoPlantilla {get set}
  func metodo() -> TipoPlantilla
}
struct MiEstructura: NombreDelProtocolo {
  typealias TipoPlantilla = String
  var propiedad: String
  func metodo() -> String {
    "cualquier cosa"
  }
}
Enter fullscreen mode Exit fullscreen mode

Protocolos más usados de Swift

Equatable

Tipo de dato que puede ser comparado por valor. Al conformar este protocolo en una estructura, el compilador genera automáticamente un método que compara todas las propiedades.

Comparable

Tipo de dato que puede ser relacionado con operadores >, <, >=, <=. Al conformar este protocolo en una estructura, solo es obligatorio definir el operador < porque por defecto el == se calcula comparando las propiedades, y > sería la operación restante.

Numeric

Tipo de dato que soporta multiplicaciones.

Hashable

Hashable es un tipo de dato que puede ser usado en un "hasher" para producir un valor entero aleatorio.

Un valor de resumen es un número entero producido a partir del valor de las propiedades de una entidad. Si todas las propiedades son Hashable, entonces al conformar Hashable se puede calcular el hash por defecto a partir de ellas sin hacer nada manualmente. Sin embargo, si una propiedad no es Hashable, se debe calcular el hash con el método hash(into: inout Hasher), que resumen los componentes esenciales de una entidad, pasándoselos al hasher dado.

Para calcular el valor de resumen se usa la estructura Hasher, recibida en el método hash(into:), que contiene el método combine(_:) que mezcla las partes esenciales del valor dado en el hasher.

var hasher = Hasher()
hasher.combine(23)
hasher.combine("Hello")
let hashValue = hasher.finalize()
Enter fullscreen mode Exit fullscreen mode

Aunque en principio el resumen ("hash") para una misma entrada debería ser el mismo, la estructura Hasher tiene una semilla aleatoria diferente en cada ejecución del programa. Esto quiere decir que en dos ejecuciones distintas del programa, posiblemente se obtendrá un resumen diferente dada la misma entrada. No obstante, sí se garantiza que en la misma ejecución del programa, se obtiene el mismo resumen para la misma entrada.

En caso de buscar uniformidad en la generación del resumen entre dos ejecuciones del programa, se debe usar un algoritmo determinista como SHA256.

CaseIterable

Tipo que produce una colección con todos los valores de una enumeración.

Restricciones de tipo ("Type constraint")

Se puede hacer que el tipo de dato plantilla de una función genérica esté restringido a conformar un protocolo específico. A esto se lo denomina "restricción de tipo" porque un tipo de dato genérico tiene una restricción que lo obliga a tener ciertas características específicas.

func compararValores<T: Equatable>(value1: T, value2: T) -> String {
  value1 == value2 ? "Iguales" : "Diferentes"
}

func sumarValores<T: Numeric>(value1: T, value2: T) -> T {
  value1 + value2
}
sumarValores(value1: 1.1, value2: 1.0) // 2.1
Enter fullscreen mode Exit fullscreen mode

Extensiones

Los protocolos solo declaran atributos y comportamiento, pero no lo definen. Sin embargo, se puede definir un comportamiento por defecto común a todas las implementaciones, a través de una extensión del protocolo.

protocol Printable {
  var name: String { get set }
}
extension Printable {
  func description() -> String {
    "Nombre: \(name)"
  }
}
Enter fullscreen mode Exit fullscreen mode

Lugar de declaración de un método

protocol Printable {
  func print() -> String
}
extension Printable {
  func print() -> String {
    "Hola"
  }
}
struct UnaEstructura: Printable {
  func print() -> String {
    "Adios"
  }
}
let e: Printable = UnaEstructura()
let message = e.print()
print(message)
Enter fullscreen mode Exit fullscreen mode

El lugar donde se declara un método (sea dentro del protocolo o solo dentro de la extensión) importa porque se pueden tener resultados diferentes. Considerar el ejemplo anterior para analizar los siguientes dos casos:

  1. Si el método func print() -> String está definido dentro de Printable, el mensaje impreso es "Adiós" porque UnaEstructura implementa dicho método, incluso aunque haya una implementación por defecto en la extensión de Printable.

  2. Si el método func print() -> String NO está definido en Printable sino solo en la extensión, el mensaje impreso es "Hola" porque UnaEstructura no está implementando un método de Printable, así que no hay despacho dinámico para este método. En su lugar, se despacha el método de la extensión de forma estática.

Extensiones condicionales

Si tenemos una estructura genérica, se puede agregar una extensión para un tipo de dato específico por medio de la cláusula where.

struct Dato<T> {
    var value: T
}
extension Dato where T == Int {
    func duplicar() -> T {
        value * 2
    }
}

let d1 = Dato(value: 2)
let result = d1.duplicar()
print(result)

let d2 = Dato(value: "Hola")
// d2.duplicar() // No existe
Enter fullscreen mode Exit fullscreen mode

Top comments (0)