1. WORKSHOP IOS:
Closures, generics & operators
Organizadores
Alberto Irurueta Carro
Objetivos
Entender el funcionamiento de generics
Revisar el uso de Protocols y operadores con generics
REPOSITORIO
El repositorio del workshop lo podéis encontrar en:
https://github.com/albertoirurueta/swift-protocols-and-generics-workshop
Generics
Generics permite implementar código en Swift que puede ser reutilizado con distintos tipos de datos (e
incluso para tipos de datos que no estén definidos pero sigan algún tipo de protocolo).
Por ejemplo, los arrays y diccionarios en Swift son structs que usan generics, de modo que podemos hacer
instancias para contener cualquier tipo de datos.
Para crear una clase que utilice generics, debe usarse la sintaxis <Tipo1, Tipo2, ...> donde Tipo1, Tipo2
son los tipos a los que nos referiremos dentro de una clase o función.
Si queremos restringir que que los tipos genéricos sean una subclase de un tipo determinado, o
implementen cierto protocolo puede usarse la sintaxis <Tipo : Protocolo>, <Tipo : Clase>
2. Ejemplo:
Container.swift
import Foundation
/**
Contenedor de un objeto cualquiera de tipo T.
*/
public class Container<T> {
/**
Valor interno almacenado por esta clase.
*/
private var _value: T
/**
Constructor.
- parameter value: valor inicial proporcionado.
*/
public init(value: T) {
_value = value
}
/**
Obtiene o establece el valor interno de esta clase.
*/
public var value: T {
get{
return _value
}
set {
_value = newValue
}
}
}
En el código de este ejemplo, podemos crear instancias de Container para contener cualquier tipo de objeto.
Swift infiere el tipo T a partir del objeto proporcionado en el constructor, tal y como se ve en el siguiente
test:
ContainerTests.swift
...
func testConstructorAndValue() {
let value1 = "hello"
let container1 = Container<String>(value: value1)
//comprobamos valor inicial
XCTAssertEqual(value1, container1.value)
//establecemos nuevo valor
container1.value = "bye"
//comprobamos
3. XCTAssertEqual(container1.value, "bye")
let container2 = Container<Int>(value: 1)
//valor inicial
XCTAssertEqual(container2.value, 1)
//nuevo valor
container2.value = 10
//comprobamos
XCTAssertEqual(container2.value, 10)
}
Operadores
public protocol Equatable {
public static func ==(lhs: Self, rhs: Self) -> Bool
}
public protocol Comparable : Equatable {
public static func <(lhs: Self, rhs: Self) -> Bool
public static func <=(lhs: Self, rhs: Self) -> Bool
public static func >=(lhs: Self, rhs: Self) -> Bool
public static func >(lhs: Self, rhs: Self) -> Bool
}
public func ><T : Comparable>(lhs: T, rhs: T) -> Bool
public func <=<T : Comparable>(lhs: T, rhs: T) -> Bool
public func >=<T : Comparable>(lhs: T, rhs: T) -> Bool
Sorter
En el repositorio hemos creado una serie de clases que utilizan generics, closures y protocols para ordenar
cualquier tipo de datos.
Se ha creado una clase base Sorter, y subclases con distintos algoritmos de ordenación implementados
(StraightInsertationSorter, ShellSorter, QuicksortSorter, HeapsortSorter, FoundationSorter)
En el repositorio está el código completo de estas clases, pero aquí os pongo algunos aspectos interesantes
a considerar:
En Swift no existen las clases abstractas. O bien se utiliza un protocol, o bien usamos un constructor
privado o internal para impedir la instanciación directa, como se ha hecho en el caso de Sorter.
Sorter.swift
/**
Constructor.
Es privado para impedir la instanciación directa de esta clase.
*/
internal init() { }
4. Por comodidad en Sorter se han añadido métodos estáticos de factoría para crear instancias de
subclases según el algoritmo de ordenación deseado.
/**
Método de factoría para crear instancias de Sorter que utilicen el método
de ordenación indicado.
- param method: método de ordenación.
*/
public static func create(method: SortingMethod) -> Sorter {
switch method {
case .foundation:
return FoundationSorter()
case .heapsort:
return HeapsortSorter()
case .quicksort:
return QuicksortSorter()
case .shell:
return ShellSorter()
case .straightInsertion:
return StraightInsertionSorter()
}
}
/**
Método de factoría para crear instancias de Sorter con el método de
ordenación por defecto.
*/
public static func create() -> Sorter {
return create(method: DefaultSortingMethod)
}
Errores: Hemos definido un enumerador que implementa Error para gestionar los errores que
algunos métodos pueden lanzar en algunas situaciones
/**
Tipos de error que pueden obtenerse al ordenar arrays.
*/
public enum SorterError : Error {
/**
Si la posición fromIndex es mayor que la posición toIndex
*/
case illegalIndices
/**
Si se proporcionan valores de índices fuera del rango de índices
accesibles por el array proporcionado (entre 0 y su count - 1).
*/
case indexOutOfBounds
/**
Si la ordenación falla por cualquier otro motivo.
*/
case sortingError
}
Algunos métodos modifican los parámetros de entrada. Por defecto en Swift los parámetros de
entrada se pasan por copia, lo cual, en el caso de los structs implica que si se modificasen, el
5. resultado no quedaría reflejado tras la ejecución de un método. Para ello puede indicarse que un
parámetro es inout para pasar tanto clases como structs por referencia.
public func sort<T>(_ array: inout [T], fromIndex from: Int,
toIndex to: Int, comparator: Comparator) throws {
try sort(&array, fromIndex: from, toIndex: to,
comparator: { (o1, o2) -> Int in
return comparator.compare(o1, o2)
})
}
Para pasar un parámetro inout por referencia, dbee usarse &, como se ve en el ejemplo anterior
Si un método lanza algún tipo de excepción o ejecuta un método con try sin recoger su excepción,
debe indicarse mediante throws en su declaración
public override func sort<T>(_ array: inout [T], fromIndex: Int,
toIndex: Int,
comparator: (_ o1: T, _ o2: T) -> Int) throws {
guard fromIndex <= toIndex else {
throw SorterError.illegalIndices
}
guard fromIndex >= 0 && toIndex <= array.count else {
throw SorterError.indexOutOfBounds
}
...
Para llamar a un método que lanza excepciones, debe usarse try, try? o try!. Try nos permite
relanzar una excepción o bien llamar al método dentro de un bloque do... catch para capturarla.
try? nos permite obtener el resultado de un método que puede lanzar una excepción como un
optional, u obtener nil si se produce la excepción, try! permite obtener el resultado de la ejecución
del método asumiendo que éste nunca va a fallar (si se produjese una excepción se detendría la
ejecución).
public func sort<T : Comparable>(_ array: inout [T], fromIndex from: Int,
toIndex to: Int) throws {
try sort(&array, fromIndex: from, toIndex: to,
comparator: { (o1, o2) -> Int in
return Sorter.compare(o1, o2)
})
}
...
try! sorter.sort(&array, fromIndex: fromIndex, toIndex: toIndex)
Si el método devuelve void, también puede usarse do... catch{} pero se considera mala práctica
no hacer nada en un bloque catch y además empeora la cobertura
public func sort<T>(_ array: inout [T],
comparator c: (_ o1: T, _ o2: T) -> Int) {
do {
try sort(&array, fromIndex: 0, toIndex: array.count, comparator: c)
} catch { }
}
6. En el sorter hay métodos sort que usan closures, que usan Protocols, que usan generics de tipos
Comparables. En el caso de las closures y los protocols, nos permiten definir cómo se realiza la
comparación entre CUALQUIER tipo de datos, y permiten incluso realizar cosas como invertir el
orden de ordenado. En el caso de generics con comparables, se nos simplifica el uso de la clase,
ya que no es necesario proporcionar un comparador.
En un for puede indicarse un range para indicar los valores sobre los que se iteran
Sin embargo, si el valor inicial es mayor que el final, el range no puede crearse y la ejecución falla.
Si se necesitase crear un range en sentido decreciente, puede hacerse con la función stride (from:
to: by:) o stride(from: through: by:). En el primer caso el valor final es exclusivo y en el segundo
es inclusivo. En cualquier caso, recordad que un for siempre puede expresarse como un while junto
con variables sobre las que se iteran.
En el caso de FoundationSorter se nos proporcionan parámetros o1 y o2 de tipo T, que no tienen
por qué ser Comparables. Podemos forzar que T sea Comparable con una función auxiliar que
defina un generic como T como Comparable y haciendo el cast dentro de un if para así poder usar
los operadores < y > con un tipo T.
public override func sort<T>(_ array: inout [T], fromIndex: Int,
toIndex: Int,
comparator: (_ o1: T, _ o2: T) -> Int) {
array.sort { (o1, o2) -> Bool in
FoundationSorter.compare(o1, o2, Int.self)
}
}
private static func compare<T: Comparable>(_ o1: Any, _ o2: Any,
_ type: T.Type) -> Bool{
if let c1 = o1 as? T, let c2 = o2 as? T {
return c1 < c2
} else {
return false
}
}
Para poder implementar Comparators que puedan usar los operadores < y > en SwiftSorter hemos
definido las siguientes clases auxiliares donde se hace un cast de un tipo T cualquiera a un tipo U
comparable
class ComparatorAscendant<U : Comparable> :
swiftProtocolsAndGenerics.Comparator {
public func compare<T>(_ o1: T, _ o2: T) -> Int {
if let c1 = o1 as? U, let c2 = o2 as? U {
if c1 < c2 {
return -1
} else if c1 == c2 {
return 0
} else {
return 1
}
} else {
return -1
}
}
}
7. class ComparatorDescendant<U : Comparable> :
swiftProtocolsAndGenerics.Comparator {
public func compare<T>(_ o1: T, _ o2: T) -> Int {
if let c1 = o1 as? U, let c2 = o2 as? U {
if c1 < c2 {
return 1
} else if c1 == c2 {
return 0
} else {
return -1
}
} else {
return -1
}
}
}
class ComparatorAndAveragerAscendant<U : Comparable> :
swiftProtocolsAndGenerics.ComparatorAndAverager {
public func compare<T>(_ o1: T, _ o2: T) -> Int {
if let c1 = o1 as? U, let c2 = o2 as? U {
if c1 < c2 {
return -1
} else if c1 == c2 {
return 0
} else {
return 1
}
} else {
return -1
}
}
public func average<T>(_ o1: T, _ o2: T) -> T {
if let i1 = o1 as? Int, let i2 = o2 as? Int {
return ((i1 + i2) / 2) as! T
} else {
return 0 as! T
}
}
}
También se han creado las clases auxiliares para el caso de Ints
class IntComparatorAscendant : ComparatorAscendant<Int> { }
class IntCompaaratorDescendant : ComparatorDescendant<Int> { }
class IntComparatorAndAveragerAscendant :
ComparatorAndAveragerAscendant<Int> { }