Esta es la parte 2 de un post en dos partes.
La parte 1 aquí
Programación orientada a objetos
En un programa tenemos dos conceptos perfectamente definidos: Datos y Comportamiento.
Una tupla con cinco campos que define una dirección es Dato. Una función que toma esa tupla y devuelve un String
con su descripción, es Comportamiento.
Todo lo que hemos hecho hasta el momento en la primera parte de la introducción a Swift ha sido definir los Datos por un lado y el Comportamiento por el otro, lo cual nos ha funcionado bien. Sin embargo, la forma más frecuente de organizar el código en los lenguajes modernos, tales como Swift es integrando Datos y Comportamiento en estructuras como las que veremos hoy, cuando estudiemos Programación orientada a objetos (POO).
Esto nos dará algunas ventajas. Principalmente, la capacidad de abstraer. La clase tendrá comportamiento que se podrá utilizar desde afuera de ella. Es decir, sabremos qué hace la clase. Sin embargo, el cómo se lo reserva la clase para sí. Esto se llama encapsulamiento.
Class
Una clase es una de las estructuras que integran datos y comportamiento en una unidad. Los datos en una clase se llaman Atributos. El comportamiento en una clase se modela por medio de funciones llamadas Métodos.
En Swift, una clase se define con la palabra clave class
seguida por su nombre. Los atributos serán variables, y los métodos, funciones.
Veamos un ejemplo de una jerarquía de clases, y luego explicaremos en detalle qué sucede:
class Animal {
var nombre: String
var paseos: [String]
var tipo: String { return "" }
init(nombre: String) {
self.nombre = nombre
self.paseos = []
}
func emitirSonido() {}
func pasear(a lugar: String) {
self.paseos.append(lugar)
}
}
class Perro: Animal {
override var tipo: String { return "Perro" }
override func emitirSonido() {
print("\(nombre): Guau!")
}
}
class Gato: Animal {
override var tipo: String { return "Gato" }
override func emitirSonido() {
print("\(nombre): Miau!")
}
}
class Persona {
private var nombre: String
private var mascotas: [Animal]
init(nombre: String, mascotas: [Animal]) {
self.nombre = nombre
self.mascotas = mascotas
}
func llegarACasa() {
print("\(nombre): llega a casa")
for mascota in mascotas {
mascota.emitirSonido()
}
}
}
let romina = Persona(
nombre: "Romina",
mascotas: [
Gato(nombre: "Fausto"),
Perro(nombre: "Lupi")
]
)
romina.llegarACasa()
Imprimirá:
Romina: llega a casa
Fausto: Miau!
Lupi: Guau!
Herencia
Bien, ¿qué sucedió aquí? Primero, definimos una clase base, llamada Animal
. Se denomina clase base porque es la clase que define la base de la jerarquía de clases. Animal
define la funcionalidad común a todos los animales.
¿Qué puede hacer un animal? (comportamiento). En este caso puede salir a pasear y puede emitir un sonido. ¿Qué sonido? No lo sabemos. El sonido que hará el animal será definido en las subclases. Esto se denomina herencia. Un animal puede ser un Gato
o un Perro
en este ejemplo. Entonces un Gato
puede decir "Miau" y definir su tipo como "Gato", pero Hereda todo el comportamiento definido en su clase base.
¿Qué podemos saber de un animal? (datos). En este caso, su nombre y los paseos que ha realizado.
¿Y tipo
? Es una variable computada, o getter. Un getter es una función que devuelve un valor. Desde fuera de la clase se ve y se usa como una variable a la cual no se le puede cambiar el valor. Forma parte del comportamiento de la clase.
Encapsulamiento
Una clase solo expone la parte de su comportamiento y sus datos que debemos utilizar desde afuera. Lo que es interno de la clase se define con la palabra clave private
. Todo lo que sea private
solo puede ser llamado desde dentro de la clase. Lo que no se marca como private
es accesible desde cualquier parte de la aplicación y se denomina interfaz pública de la clase.
Init
El init
es un método especial de la clase que se encarga de definir cómo se creará un objeto de esa clase. En este ejemplo, la clase Persona
tiene un init
definido así:
init(nombre: String, mascotas: [Animal]) {
self.nombre = nombre
self.mascotas = mascotas
}
self
en este caso se refiere al propio objeto en el que estamos operando.
El init
no necesita ser escrito para utilizarlo. Vean el ejemplo:
let romina = Persona(
nombre: "Romina",
mascotas: [
Gato(nombre: "Fausto"),
Perro(nombre: "Lupi")
]
)
Polimorfismo
La funcionalidad descrita en la clase base puede ser redefinida con la palabra clave override
en las clases hijas. Por ejemplo, emitirSonido
no tiene funcionalidad real en la clase base o clase padre. Pero en las clases hijas sí. Las clases hijas "overridean" el comportamiento de la clase padre. Por ejemplo:
override func emitirSonido() {
print("\(nombre): Guau!")
}
Lo que es interesante es que nos podemos referir tanto a perros como a gatos en el Array
de mascotas
. Cuando llamamos a mascota.emitirSonido()
no sabemos si la mascota es un perro o un gato, eso se define en tiempo de ejecución, donde cada objeto ejecuta su funcionalidad según su clase específica.
Protocol
Una class
responde a la pregunta "¿Qué es?".
¿Qué es? Un Gato
¿Qué es? Un Perro
¿Qué es? Una Persona
Un protocol
responde a la pregunta "¿Qué puede hacer?". En su forma más simple, un protocolo (también llamado interface en otros lenguajes de programación como Java), es un conjunto de firmas de métodos bajo un nombre. Cualquier clase puede implementar un protocolo. Si una clase implementa un protocolo, entonces se compromete a implementar todos los métodos definidos en él. De lo contrario, el código no compilará.
Veamos un ejemplo
protocol EmiteSonido {
func emitirSonido()
}
class Animal {}
class Perro: Animal, EmiteSonido {
func emitirSonido() {
print("Guau!")
}
}
class Gato: Animal, EmiteSonido {
func emitirSonido() {
print("Miau!")
}
}
class Timbre: EmiteSonido {
func emitirSonido() {
print("Ring!")
}
}
Noten aquí que:
- Una clase puede heredar de una sola clase. Sin embargo, puede implementar cualquier cantidad de protocolos como necesitemos. Si una clase hereda y también implementa protocolos, lo que vaya a la derecha del
:
debe ser primero la clase padre, y luego los protocolos a implementar. - Un Perro, un Gato y un Timbre ahora son del mismo tipo. Todos son del tipo
EmiteSonido
. Por lo tanto, podemos usarlos en unArray
, por ejemplo.
class EfectosDeSonido {
let sonidos: [EmiteSonido]
init(sonidos: [EmiteSonido]) {
self.sonidos = sonidos
}
func reproducir() {
for sonido in sonidos {
sonido.emitirSonido()
}
}
}
let sonidosLlegadaACasa = EfectosDeSonido(sonidos: [Timbre(), Gato(), Perro()])
sonidosLlegadaACasa.reproducir()
// Ring!
// Miau!
// Guau!
Al implementar EmiteSonido
, todos los objetos, sin importar su clase, pueden servir al propósito de emitir un sonido. En este ejemplo, esto nos sirve para implementar efectos de sonido. En la práctica, este mismo principio se usa muchísimo. Sobre todo cuando lo que necesitamos de otro objeto es lo que puede hacer, sin importar qué sea en verdad.
Struct
Una struct
o es estructura es muy similar a una clase. Veamos algunas características:
Una clase:
- Tiene atributos y métodos
- Los atributos y métodos que contiene pueden ser privados
- Puede implementar protocolos.
- Debe implementar un método
init
para inicializar sus atributos al instanciarse. - Puede heredar de otras clases.
Una estructura:
- Tiene atributos y métodos
- Los atributos y métodos que contiene pueden ser privados
- Puede implementar protocolos.
-
NO ES NECESARIO que implemente un método
init
para inicializar sus atributos por lo general. - NO PUEDE HEREDAR de ninguna clase o estructura.
Una clase tampoco puede heredar de una estructura.
protocol Describible {
func obtenerDescripcion() -> String
}
struct Direccion: Describible {
let calle: String
let numero: String
let departamento: String
let piso: String
let ciudad: Ciudad
func obtenerDescripcion() -> String {
return "\(calle) \(numero) - Dto. \(piso)-\(departamento) - \(ciudad.obtenerDescripcion())"
}
}
struct Ciudad: Describible {
let nombre: String
let provincia: Provincia
func obtenerDescripcion() -> String {
return "\(nombre), \(provincia.obtenerDescripcion())"
}
}
struct Provincia: Describible {
let nombre: String
let pais: String
func obtenerDescripcion() -> String {
return "\(nombre), \(pais)"
}
}
let direccionOficina = Direccion(
calle: "Avenida Rivadavia",
numero: "18451",
departamento: "1",
piso: "PB",
ciudad: Ciudad(
nombre: "Morón",
provincia: Provincia(
nombre: "Buenos Aires",
pais: "Argentina"
)
)
)
print(direccionOficina.obtenerDescripcion()) // Avenida Rivadavia 18451 - Dto. PB-1 - Morón, Buenos Aires, Argentina
En la práctica, las struct
se utilizan para diseñar trozos pequeños de información. Pueden servirnos para describir una dirección, los datos de una tarjeta de crédito, etc.
Enum
Una struct
o una class
nos permiten diseñar a partir del "y". Por ejemplo, una struct Persona
tiene nombre
y edad
y email
como sus miembros.
Una enum
, en cambio, nos permite diseñar a partir del "o". Por ejemplo, una enum Provincia
tiene buenosAires
o entreRios
o catamarca
como sus miembros.
Al contrario de las enum
en lenguajes como C, las enum
de Swift pueden llegar a resultar MUY complejas. De todos modos, aquí usaremos las funcionalidades básicas.
Veamos un ejemplo simple:
class Usuario {
var nombre: String
var tipoCredenciales: TipoCredenciales
init(
nombre: String,
tipoCredenciales: TipoCredenciales
) {
self.nombre = nombre
self.tipoCredenciales = tipoCredenciales
}
}
// La enum TipoCredenciales define con qué medio el usuario creó su cuenta.
enum TipoCredenciales {
case email
case facebook
case apple
case google
}
Con TipoCredenciales
podemos decir por ejemplo que un usuario se registró por email
, por facebook
, por apple
o por google
. Es imposible que un usuario se haya registrado por más de una vía (al menos en el contexto de este dominio ficticio), y el mismo código lo hace imposible.
Switch en enum
Bien, intentemos ahora utilizar TipoCredenciales
para nuestra lógica. La forma más típica de utilizar una enum
en una lógica, por ejemplo dentro de una función, es mediante la cláusula switch
.
func esUsuarioDeRedesSociales(_ usuario: Usuario) -> Bool {
switch usuario.tipoCredenciales {
case .email:
return false
case .facebook, .apple, .google:
return true
}
}
Nótese que también podríamos haber escrito esta función como un método dentro del usuario. También, que aquí combinamos varias opciones dentro de un mismo case
, pero que bien podríamos haber escrito un case
por cada uno de los métodos (facebook, apple, google).
Métodos y atributos en enum
Una enum
puede tener métodos y atributos, tal como las clases. El patrón más común para ello es incluir un switch self
dentro del mismo. Veamos un ejemplo:
enum Pais {
case argentina, alemania, inglaterra, mexico, camerun
// Crearemos una propiedad computada. Esto es un `getter`.
// Noten que esto es lo mismo que decir
//
// func esEuropeo() -> Bool { ... }
//
// Salvo que al llamarse no usaremos paréntesis, tal y como si se tratase de una
// propiedad común y corriente del objeto.
var esEuropeo: Bool {
// switch self es MUY común en estos casos.
switch self {
// Si estamos llamando esta propiedad computada desde los cases 'alemania' y 'inglaterra'
// entonces devolveremos true
case .alemania, .inglaterra:
return true
// En cambio, si lo hacemos desde cualquier otro case de la enum, entonces
// devolveremos false.
default:
return false
}
}
// Similar al caso anterior pero con un String.
var nombre: String {
switch self {
case .argentina:
return "Argentina"
case .alemania:
return "Alemania"
case .inglaterra:
return "Inglaterra"
case .mexico:
return "México"
case .camerun:
return "Camerún"
}
}
}
func describir(pais: Pais) {
if pais.esEuropeo {
print("\(pais.nombre) es europeo")
} else {
print("\(pais.nombre) NO es europeo")
}
}
describir(pais: Pais.alemania) // Alemania es europeo
Otra curiosidad con respecto a las enum
es que cuando requerimos el uso de una, como en este caso en la función describir
, no es necesario especificar el tipo. Por ejemplo, Pais.alemania
, podría haber sido tranquilamente solo .alemania
.
describir(pais: .argentina) // Es igual que describir(pais: Pais.argentina), y se ve más natural.
rawValue
Existe una forma alternativa de definir el nombre del país, como vimos en el ejemplo anterior, y es por medio de un rawValue
. Cada enum
puede tener un único rawValue
para cada uno de sus case
, y todos deben ser del mismo tipo.
El rawValue
no nos permite únicamente obtener un valor asociado a cada uno de los case
, sino también construir un case
de la enum
a partir de ese valor asociado.
Veamos un ejemplo:
enum Provincia: String {
case cordoba = "Córdoba"
case buenosAires = "Buenos Aires"
case santaFe = "Santa Fe"
}
let provincia1 = Provincia.cordoba
print("La provincia 1 es \(provincia1.rawValue)")
// Usamos el rawValue para obtener el valor de la provincia
// esto devolverá "La provincia 1 es Córdoba"
let provincia2 = Provincia(rawValue: "Buenos Aires")
// Aquí estamos haciendo el proceso inverso.
// En vez de obtener el rawValue de una Provincia,
// creamos la provincia a partir de un rawValue.
//
// Como podemos confundirnos, no hay garantía de que exista
// una provincia con ese nombre, y el tipo de provincia2
// es `Provincia?`, una Provincia opcional.
if let provincia = provincia2 {
print("Pudimos obtener la provincia2 y es \(provincia.rawValue)")
} else {
print("No pudimos obtener la provincia2, y es nil")
}
Valores asociados
Este es el último caso que veremos aquí sobre las enum
. Lo quiero remarcar porque es algo muy utilizado en Swift, aunque no entra dentro del scope del curso. Quiero decir, no es necesario saberlo para poder desarrollar la aplicación que haremos aquí. Sin embargo, saber esto puede llegar a resultar muy útil.
Cada caso de una enum puede llegar a tener valores asociados que difieren unos de otros. Por ejemplo, imaginemos un paquete de galletitas surtidas. Las galletitas podrían estar representadas por una enum
. Puedo estar confundiéndome entre galletitas, fanboys/fangirls de galletitas surtidas por favor tener paciencia.
enum Galletita {
// Cada uno de los case puede tener cualquier cantidad de valores asociados. Cada uno puede opcionalmente
// tener un nombre asociado, y puede ser del tipo que se desee.
case sonrisas(saborDeMasa: SaborDeMasa)
case rellena(relleno: Relleno, saborDeMasa: SaborDeMasa)
case bocaDeDama
var descripcion: String {
switch self {
// Al momento de realizar un switch sobre la enum, podemos
// obtener los valores asociados por medio de `let`.
case .sonrisas(let saborDeMasa):
return "Sonrisa de \(saborDeMasa.descripcion)"
case .rellena(let relleno, let saborDeMasa):
return "Galletita rellena de \(relleno.descripcion), sabor \(saborDeMasa.descripcion)"
case .bocaDeDama:
return "Boca de dama"
}
}
}
enum Relleno {
case vainilla, frambuesa
var descripcion: String {
switch self {
case .vainilla: return "vainilla"
case .frambuesa: return "frambuesa"
}
}
}
enum SaborDeMasa {
case vainilla, chocolate
var descripcion: String {
switch self {
case .vainilla: return "vainilla"
case .chocolate: return "chocolate"
}
}
}
struct PaqueteDeGalletitas {
let galletitas: [Galletita]
func describir() {
print("-")
print("Paquete:")
for galletita in galletitas {
print(galletita.descripcion)
}
}
}
let surtido = PaqueteDeGalletitas(galletitas: [
.bocaDeDama,
.rellena(relleno: .frambuesa, saborDeMasa: .chocolate),
.rellena(relleno: .frambuesa, saborDeMasa: .chocolate),
.rellena(relleno: .frambuesa, saborDeMasa: .chocolate),
.sonrisas(saborDeMasa: .chocolate),
.sonrisas(saborDeMasa: .vainilla),
.sonrisas(saborDeMasa: .vainilla),
.bocaDeDama,
.bocaDeDama,
.sonrisas(saborDeMasa: .chocolate)
])
surtido.describir()
// -
// Paquete:
// Boca de dama
// Galletita rellena de frambuesa, sabor chocolate
// Galletita rellena de frambuesa, sabor chocolate
// Galletita rellena de frambuesa, sabor chocolate
// Sonrisa de chocolate
// Sonrisa de vainilla
// Sonrisa de vainilla
// Boca de dama
// Boca de dama
// Sonrisa de chocolate
Closures
Los closure
o clausuras son también llamadas funciones anónimas. Veamos el concepto primero de función como tipo de dato.
Funciones como tipo de dato
Las funciones son un tipo de dato, como Int
, Double
, Bool
, una class
, struct
o enum
. Esto implica que uno podría tomar una función y enviarla como argumento a otra función, o hacer que una función devuelva otra función como resultado, o tener una struct
donde uno de sus atributos sea una función. Esto es en realidad algo extraño si venimos de un lenguaje como Java, C o C++, pero es en realidad bastante común en otros lenguajes.
Para convertir una función en su tipo de dato correspondiente, debemos ver los tipos de los argumentos que recibe, y el tipo de dato que devuelve. Así, una función sumar:
func sumar(x: Int, y: Int) -> Int { ... }
Pasa a ser de tipo (Int, Int) -> Int
, porque recibe dos enteros y devuelve un entero. Veamos otros ejemplos:
func sumar(x: Int, y: Int) -> Int { ... } // (Int, Int) -> Int
func describir(a persona: Persona) { ... } // (Persona) -> Void
func imprimirHoraActual() { ... } // () -> Void
func obtenerFechaActual() -> String { ... } // () -> String
Y, como dijimos, un tipo de dato función puede usarse como argumento para otras funciones:
struct Persona {
let id: Int
let nombre: String
let ocupacion: Ocupacion?
let edad: Int
}
enum Ocupacion {
case desarrollador, projectManager, contador, abogado
}
func imprimirPersonasMayoresDeEdad(_ personas: [Persona]) {
for persona in personas {
if persona.edad >= 18 {
print("\(persona.id) - \(persona.nombre)")
}
}
}
let personas = [
Persona(id: 1, nombre: "Franco", ocupacion: .abogado, edad: 34),
Persona(id: 2, nombre: "Gimena", ocupacion: .projectManager, edad: 24),
Persona(id: 3, nombre: "Gonzalo", ocupacion: .abogado, edad: 26),
Persona(id: 4, nombre: "Noelia", ocupacion: .desarrollador, edad: 29),
Persona(id: 5, nombre: "Pablo", ocupacion: nil, edad: 15),
Persona(id: 6, nombre: "Lourdes", ocupacion: .contador, edad: 29),
]
imprimirPersonasMayoresDeEdad(personas)
// Esto funcionará correctamente
//
// Sin embargo, no tenemos forma de dar 'flexibilidad' al algoritmo. Quiero decir,
// dentro de la función imprimirPersonasMayoresDeEdad filtramos por edad, y luego imprimimos.
// Si quisiéramos variar la forma de filtrar, deberíamos escribir una función completamente nueva.
// Convirtamos esa función en algo más flexible:
// Estamos "inyectando" una función dentro de otra función como argumento
func imprimir(_ personas: [Persona], si cumpleCondicion: (Persona) -> Bool) {
for persona in personas {
if cumpleCondicion(persona) {
print("\(persona.id) - \(persona.nombre)")
}
}
}
func esMayor(_ persona: Persona) -> Bool {
return persona.edad >= 18
}
imprimir(personas, si: esMayor) // Exactamente el mismo resultado. Pasamos la función como argumento en este caso.
Funciones anónimas
Ahora sí, ya con esta introducción, podemos hablar de funciones anónimas. Una función anónima o closure es una función que no tiene nombre. Tan simple como suena. Y el mejor contexto para usar funciones anónimas es para pasarlas a otras funciones. Por ejemplo, en este caso yo podría haber decidido que no tenía sentido definir una función solo para determinar si una persona es mayor.
Definámosla como función anónima:
print("Imprimiendo personas mayores de edad por closure (1):")
imprimir(
personas,
si: { (persona: Persona) -> Bool in
return persona.edad >= 18
}
)
Eso es una closure. La sintaxis puede resultar extraña y hay muchas formas de definirlas, aquí hay más ejemplos: https://fuckingclosuresyntax.com/
Por lo pronto, veamos la transformación paso a paso
Tenemos esta función:
func esMayor(_ persona: Persona) -> Bool {
return persona.edad >= 18
}
Paso 1: Le quitamos el func
y el nombre:
(_ persona: Persona) -> Bool {
return persona.edad >= 18
}
Paso 2: En caso de que sus parámetros tengan nombre interno y externo, nos quedaremos únicamente con el interno:
(persona: Persona) -> Bool {
return persona.edad >= 18
}
Paso 3: Pasamos la llave de inicio del cuerpo, al comienzo de la declaración , y en su lugar pondremos in
:
{ (persona: Persona) -> Bool in
return persona.edad >= 18
}
¡Perfecto! Ya con esto tendremos una closure definida correctamente. Podemos quedarnos aquí, pero voy a comentarles algunas otras formas de seguir acortando esa declaración. Repito, es completamente opcional:
Paso opcional 1: Quitamos el tipo del argumento, el compilador puede inferirlo en la mayoría de las situaciones.
{ (persona) in
return persona.edad >= 18
}
Paso opcional 2: Quitamos los paréntesis que envuelven a los argumentos:
{ persona in
return persona.edad >= 18
}
Paso opcional 3: Si la closure tiene una única sentencia, podemos simplemente obviar el return
.
{ persona in persona.edad >= 18 }
Paso opcional 4: En vez de usar los nombres de los argumentos (en este caso persona
), podemos referirnos a los argumentos por orden de los mismos. Por ejemplo, en este caso persona
es $0
, si tuviéramos dos argumentos, el primero sería $0
y el segundo $1
. Si tuviéramos cuatro, serían $0
, $1
, $2
, $3
, y así sucesivamente:
{ $0.edad >= 18 }
Y más que eso no podemos acortarlo
print("Mayores de edad que son abogados")
imprimir(personas, si: { $0.edad >= 18 && $0.ocupacion == .abogado })
Si tenemos la closure como último argumento de la función, entonces podemos sacarla por fuera de la llamada a la función. Quedará más claro con un ejemplo:
print("Mayores de edad que son abogados (2)")
imprimir(personas) { $0.edad >= 18 && $0.ocupacion == .abogado } // Exactamente igual al ejemplo anterior.
Map, filter, sorted y forEach
Hay funciones dentro de la biblioteca de funciones estándar que provee Swift que nos permiten pasar funciones a otras funciones, especialmente en el caso de tratamiento de Array
.
-
map
es una función que nos permite transformar cada elemento del array en otro elemento pasándole una función que realiza la transformación para cada elemento. -
filter
es una función que nos permite filtrar un array pasándole una función que devuelvetrue
en caso de que el elemento tenga que ir en elArray
de detino, ofalse
en caso contrario -
sorted
es una función que nos permite ordenar un array. Funciona de forma similar alfilter
, donde devolvemostrue
en caso de que un elemento deba ir primero que otro en el array de destino. La función desort
recibe dos argumentos para compararlos, y no uno solo. -
forEach
, por último nos permite iterar sobre un array, realizando algo en el array de origen. Esta función no devuelve nada, al contrario demap
,filter
ysorted
.
Es importante destacar que cada una de estas funciones (salvo forEach
) devuelven un array completamente nuevo. No realizan modificaciones sobre el mismo array.
let personasMayores = personas.filter { $0.edad >= 18 }
let nombres = personas.map { $0.nombre }
let personasOrdenadasPorEdad = personas.sorted { $0.edad < $1.edad }
// También podemos "encadenar" estas funciones, ya que cada una devolverá un `Array` nuevo.
print("Funciones encadenadas:")
personas
.filter { $0.edad >= 18 } // Solo consideramos las personas mayores de edad
.sorted { $0.edad < $1.edad } // Luego las ordenaremos por edad
.map { $0.nombre } // Y solo nos importará el nombre
.forEach { print($0) } // Finalmente, imprimiremos sus nombres
// Gimena
// Gonzalo
// Noelia
// Lourdes
// Franco
Extensions
Las extensions nos permiten extender un tipo existente para agregarle funcionalidades nuevas. Estas funcionalidades pueden ser propiedades computadas o métodos.
Veamos un ejemplo sencillo:
struct Direccion {
let calle: String
let numero: String
let ciudad: String
}
extension Direccion {
var descripcion: String {
return "\(calle) \(numero) - \(ciudad)"
}
}
let direccion = Direccion(calle: "Rivadavia", numero: "18451", ciudad: "Morón")
print(direccion.descripcion) // Rivadavia 18451 - Morón
Lo de recién es exactamente igual a esto:
struct Direccion {
let calle: String
let numero: String
let ciudad: String
var descripcion: String {
return "\(calle) \(numero) - \(ciudad)"
}
}
Una extension
solo brinda la posibilidad de extender la funcionalidad de un tipo, sea class
, struct
, enum
, etc.
Una funcionalidad interesante de las extension
es que podemos extender también tipos nativos, tales como Int
, Double
, o String
.
extension Int {
func esMayor(que otroNumero: Int) -> Bool {
return self > otroNumero
}
}
if 10.esMayor(que: 5) {
print("10 es mayor que 5")
}
Typealias
Los typealias
son justamente alias para tipos. Esto quiere decir que podemos referirnos a un tipo con otro nombre. Por ejemplo, imaginemos que tenemos una aplicación con usuarios, donde cada usuario tiene un identificador. Este ID del usuario es un entero. Sin embargo, un typealias
puede traer mayor claridad cuando necesitemos referirnos al id del usuario.
Recordemos que un buen código es fácil de extender, y fácil de entender.
typealias IDUsuario = Int
struct Usuario {
let id: IDUsuario
let name: String
}
typealias IDInmueble = Int
typealias Direccion = (calle: String, numero: String, ciudad: String, provincia: String, pais: String)
struct Inmueble {
let id: IDInmueble
let idPropietario: IDUsuario
let direccion: Direccion
}
let oficina = Inmueble(
id: 1,
idPropietario: 10,
direccion: (
calle: "Rivadavia",
numero: "18451 PB Torre 2",
ciudad: "Moron",
provincia: "Buenos Aires",
pais: "Argentina"
)
)
Noten que podríamos haber hecho exactamente lo mismo sin typealias
. Por lo general (salvo en casos mucho más avanzados que no veremos en este curso), los typealias
brindan claridad al código, haciendo más evidente nuestra intención al diseñarlo.
Ejercicios Segunda Parte
Queremos desarrollar una aplicación de viajes. La idea de la aplicación es tener un listado de posibles destinos. El usuario puede elegir sus destinos favoritos, los que podrá ver en otra lista.
Este ejercicio consiste en desarrollar las estructuras de datos necesarias para dar soporte a la aplicación. Se requiere, al menos:
- Usar clases, structs o enums para las entidades
User
,Address
,Place
(ciudades o destinos turísticos),Landmark
(monumentos, atracciones turísticas, etc. dentro de las ciudades) - Escribir todas las entidades, atributos y métodos en inglés.
- Permitir obtener los
Place
yLandmark
favoritos de un usuario determinado.
Usar la imaginación y creatividad, y realizar el ejercicio más completo posible.
Top comments (0)