com
Josué V. Herrera
16-21 minutos
Sintaxis
La sintaxis de un protocolo es bien simple:
protocol SomeProtocol {
// La definición del protocolo va aquí
} // SomeProtocol
Nuestros tipos pueden adoptar un protocolo en particular mediante la
colocación del nombre del protocolo después del nombre del tipo, separados
por dos puntos, como parte de su definición. También podemos adoptar
múltiples protocolos sencillamente separándolos por comas:
struct SomeStructure: FirstProtocol, AnotherProtocol {
// La definición de la estructura va aquí
} // SomeStructure
…en este ejemplo podemos ver una estructura de nombre SomeStructure
que adopta dos protocolos, FirstProtocol y AnotherProtocol.
En el caso de las clases y específicamente de aquellas que heredan de una
clase base tendríamos que especificar primero la clase base y luego los
protocolos, de la siguiente forma:
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// La definición de la clase va aquí
} // SomeClass
Herencia de protocolos
Un protocolo puede heredar de uno o más protocolos y puede añadir
requisitos adicionales a aquellos heredados. La sintaxis de la herencia
protocolo es similar a la sintaxis de la herencia de clases, pero con la
posibilidad de heredar de múltiples protocolos:
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// La definición va aquí
} // InheritingProtocol
He aquí un ejemplo de un protocolo que hereda de TextRepresentable:
protocol PrettyTextRepresentable: TextRepresentable {
var prettyTextualDescription: String { get }
} // PrettyTextRepresentable
El protocolo PrettyTextRepresentable hereda los requerimientos de
TextRepresentable y añade a prettyTextualDescription, por lo que todo aquel
que adopte a PrettyTextRepresentable tendrá que implementar todos los
requerimientos que este herede y defina.
Protocolos class-only
Como parte de la flexibilidad que tienen los protocolos también los podemos
limitar a que solamente puedan ser adoptados por clases, es decir que ni las
estructuras ni las enumeraciones podrían adoptar estos protocolos. Esto lo
logramos mediante la adición de la palabra clave class a la lista de herencia
de un protocolo, esta palabra clave siempre debe aparecer en primer lugar en
la lista:
protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
// La definición va aquí
} // SomeClassOnlyProtocol
…el protocolo SomeClassOnlyProtocol solamente puede ser adoptado por
clases, este a su vez hereda del protocolo SomeInheritedProtocol y en caso
que una clase adopte su definición esto generará un error en tiempo de
compilación.
Composición de protocolos
En ciertas ocaciones puede ser útil que mediante un tipo podamos hacer
referencia a múltiples protocolos de una vez, esto lo podemos lograr a través
de una composición de protocolos. Estas composiciones tienen la forma
protocol<SomeProtocol, AnotherProtocol> y podemos listar tantos protocolos
como queramos (seguramente debe de haber un límite pero no he podido
averiguarlo) siempre dentro de los corchetes angulares (<>) y separados por
comas.
Aquí podemos ver un ejemplo donde combinamos dos protocolos (llamados
Names y Aged) en una sola composición, la cual es requerida como
parámetro en una función:
1 protocol Named {
2 var name: String { get }
3 } // Named
4 protocol Aged {
5 var age: Int { get }
6 } // Aged
7 struct Person: Named, Aged {
8 var name: String
9 var age: Int
10 } // Person
11 func wishHappyBirthday(celebrator: protocol<Named, Aged>) {
12 print("Happy birthday \(celebrator.name) - you're \(celebrator.age)!")
13
14
15
16
17
18
19
} // wishHappyBirthday
20
let birthdayPerson = Person(name: "Nery", age: 15)
21
wishHappyBirthday(birthdayPerson)
22
23
24
25
26
27
28
…de la línea 13 a la 18 hemos creado la estructura Person que adopta a los
dos protocolos que hemos definido más arriba, en la líneas 20 a la 24
tenemos a la función wishHappyBirthday la cual recibe como argumento una
composición de protocolos o lo que sería lo mismo que decir, una clase,
estructura o enumeración que adopte ambos protocolos.
Comprobando protocolos
En casos como aquel, donde tenemos un arreglo de tipos AnyObject y
queremos iterar a través de él y verificar cuales de sus elementos adoptan
cierto protocolo, en estos casos, podemos hacer lo siguiente:
1 protocol HasArea {
2 var area: Double { get }
3 } // HasArea
4 class Circle: HasArea {
5 let pi = 3.1415927
6 var radius: Double
7 var area: Double {
8 return pi * radius * radius
9 } // area
10 init(radius: Double) {
11 self.radius = radius
12 } // init
13 } // Circle
14
15
16
17
18
19
20
21
22 class Country: HasArea {
23 var area: Double
24 init(area: Double) {
25 self.area = area
26 } // init
27 } // Country
28 class Animal {
29 var legs: Int
30 init(legs: Int) {
31 self.legs = legs
32 } // init
33 } // Animal
34 let objects: [AnyObject] = [Circle(radius: 2.0), Country(area: 243_610),
35 Animal(legs: 4)]
36 for object in objects {
37 if let objectWithArea = object as? HasArea {
38 print("Area is \(objectWithArea.area)")
39 } else {
40 print("Something that doesn't have an area")
41 } // else
42 } // for
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
…al inicio creamos el protocolo HasArea seguido de tres clases, las dos
primeras adoptan este protocolo y la segunda no, ya que el área de un animal
usualmente no es una dato importante, en la línea 50 creamos un arreglo
llamado objects y lo inicializamos con una de instancia de cada una de las
clases anteriores. En las líneas 52 a la 64 es donde se encuentra la parte
interesante de este ejemplo, en estas iteramos por el arreglo, pero
específicamente en la 54 es donde comprobamos si el elemento de ese ciclo
del bucle adopta el protocolo HasArea o no, en este caso haciendo uso del
operador downcasting (as?).
En Swift podemos hacer uso de los operadores check (is) y downcasting (as?
/ as!) para comprobar si un objeto ya sea una clase, estructura o enumeración
adoptan cierto protocolo o también para hacer un casting a un protocolo en
especifico. Como hemos visto en el ejemplo anterior el chequeo y el casting
siguen exactamente la misma sintaxis que cuando hacemos lo mismo con un
tipo de dato cualquiera:
El operador is retorna true si la instancia adopta el protocolo y false en caso
contrario.
La versión as? del operador downcast retorna un valor opcional del tipo de
protocolo, este valor será nil en caso de que la instancia no adopte el
protocolo en cuestión.
En el caso de la versión as! del operador downcast se fuerza el downcast
(casting hacia el subtipo) hacia el tipo de protocolo, en caso de que el
downcast no se ejecute satisfactoriamente se genera un error en tiempo de
ejecución.
La salida en pantalla del código anterior sería:
Area is 12.5663708
Area is 243610.0
Something that doesn't have an area
Extensiones
Los protocolos pueden ser extendidos en pos de proveer métodos y
propiedades que estén incluso más acordes a ciertos tipos ya por defecto
asociados. Esto nos permite definir comportamientos en los protocolos en sí,
en lugar de en cada tipo que lo adopte, veamos un ejemplo:
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.5
} // randomBool
} // RandomNumberGenerator
…el protocolo RandomNumberGenerator puede ser extendido para
proporcionar un método randomBool, es decir un método que nos devuelve
un valor booleano aleatorio. En caso que se estén diciendo:
Esto es absurdo, ¿no sería mejor añadir randomBool junto con la definición
del protocolo?
Pues absurdo no es, ya que en la definición del protocolo ni las propiedades
ni los métodos pueden tener cuerpo y a esto se le suma el beneficio de que
podemos extender incluso protocolos propios de Swift o de terceros, es decir
protocolos no definidos por nosotros y a los que en muchos casos ni siquiera
tenemos acceso a su código fuente.
Veamos un ejemplo del segmento anterior en uso:
1 protocol RandomNumberGenerator {
2 func random() -> Double
3 } // RandomNumberGenerator
4 extension RandomNumberGenerator {
5 func randomBool() -> Bool {
6 return random() > 0.5
7 } // randomBool
8 } // RandomNumberGenerator
9 class LinearCongruentialGenerator: RandomNumberGenerator {
10 var lastRandom = 42.0
11 let m = 139968.0
12 let a = 3877.0
13 let c = 29573.0
14 func random() -> Double {
15 lastRandom = ((lastRandom * a + c) % m)
16 return lastRandom / m
17 } // random
18 } // LinearCongruentialGenerator
19 let generator = LinearCongruentialGenerator()
20 print("Here's a random number: \(generator.random())")
21
22
23
24
25
26
27
28
29
print("And here's a random Boolean: \(generator.randomBool())")
30
31
32
33
34
35
36
37
38
…este ejemplo no es nuevo, aunque sí presentamos una nueva versión del
mismo. En el podemos comprobar que no hemos modificado la definición del
protocolo ni la clase que lo adopta, lo nuevo aquí es la extensión del mismo y
la última línea donde hacemos una llamada al nuevo método añadido por la
extensión.
Resumen
Las extensiones de protocolos y las implementaciones por defecto pueden
lucir similar a usar base clases o incluso clases abstractas en otros lenguajes,
pero estas nos ofrecen algunas ventajas claves en Swift:
Debido a que podemos tener tipos que adopten más de un protocolo, estos
pueden ser decorados con comportamientos por defecto desde múltiples
protocolos. A diferencia de la herencia múltiple en el caso de las clases y que
Swift no soporta, la posibilidad de extender los protocolos no incluye ningún
estado adicional.
Los protocolos pueden ser adoptados por clases, estructuras y
enumeraciones, mientras que las clases base y le herencia están restringidas
a las clases.
En otras palabras, mediante las extensiones de protocolos podemos definir
comportamientos por defecto para tipos por valor (estructuras,
enumeraciones…) y no solo para tipos por referencia (clases).
Hasta donde tengo información, Swift es el primer lenguaje que introduce el
paradigma de la programación orientada a protocolos y con ello la intención
de un modelo de abstracción más eficiente y flexible, una apuesta directa por
los tipos por valor.
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!