Un Recorrido Por Swift
Explora las características y la sintaxis de Swift.
Es costumbre que el primer programa que se crea al aprender un nuevo lenguaje de programación es aquel que imprime la frase ¡Hola, mundo! en la pantalla. En Swift, esto se puede conseguir mediante una sola línea de código:
print("¡Hola, mundo!")
// Imprime "¡Hola, mundo!"
Si has desarollado anteriormente en C u Objective-C, esta sintaxis te resultará familiar; en Swift, esta línea de código representa un programa completo. No hace falta importar una biblioteca aparte para contar con funciones como entrada/salida o manejo de cadenas de texto. Todo código escrito en el ámbito (scope) global se utiliza como punto de entrada para el programa, por lo que no necesitamos una función main()
. Tampoco hace falta escribir punto y coma al final de cada declaración.
Esta guía te proporciona suficiente información para comenzar a desarrollar código en Swift al enseñarte cómo realizar una variedad de tareas de programación. No te preocupes si hay algo que no entiendes: todo lo presentado en esta guía se explica en detalle en el resto de este libro.
Valores sencillos
Usa let
para crear una constante y var
para crear una variable. No es necesario conocer el valor de una constante a la hora de compilar, pero tal valor debe asignarse exactamente una única vez. Esto significa que puedes usar constantes para nombrar un valor que solo se define una vez, pero que se usa en muchas partes.
var miVariable = 42
miVariable = 50
let miConstante = 42
Una constante o variable debe ser del mismo tipo que el valor que se le quiera asignar. Sin embargo, no siempre tienes que escribir el tipo explícitamente. El hecho de proporcionar un valor a una variable o constante durante su creación, le permite al compilador inferir el tipo de dicha variable o constante. En el ejemplo anterior, el compilador infiere que miVariable
es un entero porque su valor inicial es un entero.
Si el valor inicial no proporciona suficiente información (o si no hubiera un valor inicial), especifica el tipo escribiéndolo después de la variable, separado por dos puntos.
let enteroImplicito = 70
let doubleImplicito = 70.0
let doubleExplicito: Double = 70
Los valores nunca se convierten a un tipo diferente implícitamente. Si necesitas convertir un valor a un tipo diferente, debes crear —de manera explícita— una instancia del tipo deseado.
let etiqueta = "El ancho es "
let ancho = 94
let anchoDeLaEtiqueta = etiqueta + String(ancho)
Hay una manera incluso más sencilla de insertar valores en una cadena de texto — escribe el valor en paréntesis, precedidos por una barra invertida (\
). Por ejemplo:
let manzanas = 3
let naranjas = 5
let totalManzanas = "Tengo \(manzanas) manzanas."
let totalFrutas = "Tengo \(manzanas + naranjas) frutas."
Usa tres comillas dobles para cadenas de texto que ocupen más de una línea. La sangría (indentation) al inicio de cada línea de la cadena de texto es removida siempre y cuando concuerde con la sangría de las comillas de cierre. Por ejemplo:
let cita = """
Aun cuando hay espacios en blanco a la izquierda,
las líneas no contienen sangría en realidad.
Excepto por esta línea.
Las comillas dobles (") pueden aparecer sin escaparlas.
Todavía tengo \(manzanas + naranjas) frutas.
"""
Crea arrays y diccionarios usando corchetes ([]
) y accede a sus elementos referenciando su índice o clave en corchetes. Está permitido agregar una coma después del último elemento.
var frutas = ["fresas", "peras", "mandarinas"]
frutas[1] = "uvas"
var ocupaciones = [
"Manuel": "Capitán",
"Carlos": "Mecánico"
]
ocupaciones["Julia"] = "Relaciones Públicas"
Los arrays crecen automáticamente a medida que agregas elementos.
frutas.append("moras")
print(frutas)
// Imprime "[fresas, uvas, mandarinas, moras]"
También se usan corchetes para crear un array o un diccionario vacíos. Para un array, usa []
y, para un diccionario, usa [:]
.
frutas = []
ocupaciones = [:]
Si asignas un array o un diccionario vacíos a una nueva variable, o a algún otro lugar donde no hay ninguna información sobre el tipo, tendrás que especificarlo.
let arrayVacio: [String] = []
let diccionarioVacio: [String: Float] = [:]
Flujo de control
Usa if
y switch
para crear condicionales, y usa for-in
, while
, y repeat-while
para crear bucles. Los paréntesis alrededor de la condición o de la variable del bucle son opcionales. Las llaves alrededor del cuerpo son obligatorias.
let puntajesIndividuales = [75, 43, 103, 87, 12]
var puntajeDelEquipo = 0
for puntaje in puntajesIndividuales {
if puntaje > 50 {
puntajeDelEquipo += 3
} else {
puntajeDelEquipo += 1
}
}
print(puntajeDelEquipo)
// Imprime "11"
En una instrucción if
, el condicional debe ser una expresión de tipo booleano; esto significa, que una instrucción de la forma if puntaje { ... }
es un error, ya que no se compara implícitamente con cero.
Puede escribir if
o switch
después del signo igual (=
) de una asignación o después de return
, para elegir un valor en función de la condición.
let decoracionDelPuntaje = if puntajeDelEquipo > 50 {
"🎉"
} else {
""
}
print("Puntaje: ", puntajeDelEquipo, decoracionDelPuntaje)
// Imprime "Puntaje: 11 🎉"
Puedes usar if
y let
en conjunto para lidiar con valores que podrían no existir. Estos valores son representados como opcionales. Un valor opcional puede contener un valor o nil
—para indicar la ausencia de un valor—. Agrega un signo de interrogación (?
) después del tipo de un valor para señalar dicho valor como opcional.
var cadenaOpcional: String? = "Hola"
print(cadenaOpcional == nil)
// Imprime "false"
var nombreOpcional: String? = "John Appleseed"
var saludo = "¡Hola!"
if let nombre = nombreOpcional {
saludo = "Hola, \(nombre)."
}
Si el valor opcional es nil
, el condicional evalúa a false
y el código en las llaves es ignorado. En caso contrario, se extrae el valor opcional y se le asigna a la constante después de let
, lo cual hace que el valor obtenido esté disponible dentro del bloque de código.
Otra forma de manejar valores opcionales es proporcionarles un valor predeterminado mediante el uso del operador ??
. Si el valor opcional no existe, se usará el valor predeterminado en su lugar.
let alias: String? = nil
let nombreCompleto: String = "John Appleseed"
let saludoInformal = "Hola, \(alias ?? nombreCompleto)"
Puedes usar una sintaxis más corta para extraer un valor al utilizar el mismo nombre para dicho valor extraído.
if let alias {
print("Hey, \(alias)")
}
// No imprime nada porque alias es nil.
Los bucles de tipo switch
soportan cualquier tipo de datos así como una gran variedad de operaciones comparativas; estos no se limitan a enteros y pruebas de calidad.
let vegetal = "pimiento rojo"
switch vegetal {
case "apio":
print("Un par de frutos verdes más y tendrás un buen batido.")
case "pepino", "cebolla":
print("Útiles para un buen sandwich.")
case let x where x.hasSuffix("pimiento"):
print("¿Es un \(x) picante?")
default:
print("Todo sabe bien en una sopa.")
}
// Imprime "¿Es un pimiento rojo picante?"
Observa cómo es posible usar let
con un patrón para asignar el valor que concuerde con dicho patrón a una constante.
Una vez ejecutado el código dentro del caso que concuerda, el programa abandona el bucle switch. La ejecución no continúa al siguiente caso, por lo que no es necesario indicar la salida del bucle explícitamente al final del código de cada caso.
Puedes usar for-in
para iterar sobre los elementos de un diccionario al proporcionar un par de nombres que serán usados para cada par llave-valor. Los diccionarios son colecciones sin un orden particular, por lo que se itera sobre sus llaves y valores de manera arbitraria.
let numerosInteresantes = [
"Primos": [2, 3, 5, 7, 11, 13],
"Fibonacci": [1, 1, 2, 3, 5, 8],
"Cuadrados": [1, 4, 9, 16, 25]
]
var numeroMayor = 0
for (_, numeros) in numerosInteresantes {
for numero in numeros {
if numero > numeroMayor {
numeroMayor = numero
}
}
}
print(numeroMayor)
// Imprime "25"
Usa while
para repetir un bloque de código hasta que se satisfaga una condición. La condición del bucle puede ir al final, para asegurar que este se ejecute al menos una vez.
var n = 2
while n < 100 {
n *= 2
}
print(n)
// Imprime "128"
var m = 2
repeat {
m *= 2
} while m < 100
print(m)
// Imprime "128"
Es posible tener índices en un bucle al usar ..<
para crear un rango de índices.
var total = 0
for i in 0..<4 {
total += i
}
print(total)
// Imprime "6"
Usa ..<
para crear un rango que omita el valor superior, y usa ...
para crear uno que incluya ambos valores.
Funciones y clausuras
Usa func
para declarar una función. Para llamar una función, escribe una lista de argumentos en paréntesis después de su nombre. Usa ->
para separar el nombre de los parámetros y sus tipos del tipo que devuelve la función.
func saludar(persona: String, dia: String) -> String {
return "Hola, \(persona), hoy es \(dia)."
}
saludar(persona: "Bob", dia: "Martes")
Por defecto, las funciones usan los nombres de sus parámetros como etiquetas para sus argumentos. Crea tu propia etiqueta para un argumento anteponiéndola al nombre del parámetro, o agrega _
para no usar una etiqueta para un argumento.
func saludar(_ persona: String, en dia: String) -> String {
return "Hola, \(persona), hoy es \(dia)."
}
saludar("Juan", en: "Miercoles")
Usa una tupla para crear un valor compuesto; por ejemplo, para devolver múltiples valores desde una función. Los elementos de una tupla se pueden referenciar bien sea por nombre o por número.
func calcularEstadisticas(puntajes: [Int]) -> (min: Int, max: Int, suma: Int) {
var min = puntajes[0]
var max = puntajes[0]
var suma = 0
for puntaje in puntajes {
if puntaje > max {
max = puntaje
} else if puntaje < min {
min = puntaje
}
suma += puntaje
}
return (min, max, suma)
}
let estadisticas = calcularEstadisticas(puntajes: [5, 3, 100, 3, 9])
print(estadisticas.suma)
// Imprime "120"
print(estadisticas.2)
// Imprime "120"
Las funciones pueden anidarse. Las funciones anidadas tienen acceso a variables que hayan sido declaradas en la función externa. Puedes usar funciones anidadas para organizar código que resulta extenso o complejo.
func devolverQuince() -> Int {
var y = 10
func agregar() {
y += 5
}
agregar()
return y
}
devolverQuince()
Las funciones son un tipo de primera clase. Esto quiere decir que una función puede devolver otra función como su valor.
func crearIncrementador() -> ((Int) -> Int) {
func agregarUno(numero: Int) -> Int {
return 1 + numero
}
return agregarUno
}
var incrementar = crearIncrementador()
incrementar(7)
Una función puede tomar a otra función como uno de sus argumentos.
func coincideAlguno(lista: [Int], condicion: (Int) -> Bool) -> Bool {
for elemento in lista {
if condicion(elemento) {
return true
}
}
return false
}
func menorQueDiez(numero: Int) -> Bool {
return numero < 10
}
var numeros = [20, 19, 7, 12]
coincideAlguno(lista: numeros, condicion: menorQueDiez)
Las funciones son, en realidad, un caso especial de las clausuras: bloques de código que pueden ser llamados más tarde. El código en una clausura tiene acceso a elementos como variables y funciones que están disponibles en el ámbito en el cual se creó la clausura, incluso cuando la clausura se encuentra en un ámbito diferente al momento de ejecutarse; ya has visto un ejemplo de esto con las funciones anidadas. Puedes crear una clausura anónima al encerrar el código en llaves ({}
). Usa in
para separar los argumentos y el tipo devuelto por la función del cuerpo de la función.
numeros.map({ (numero: Int) -> Int in
let resultado = 3 * numero
return resultado
})
Existen muchas maneras de crear una clausura de forma más concisa. Cuando se conoce el tipo de una clausura, como es el caso de un callback para un delegado, puedes omitir el tipo de sus parámetros, el tipo devuelto, o de ambos. Las clausuras de una sola sentencia devuelven el valor de su única sentencia de manera implícita.
let numerosMapeados = numeros.map({ numero in 3 * numero })
print(numerosMapeados)
// Imprime "[60, 57, 21, 36]"
Puedes referenciar parámetros por número en vez de nombre; este enfoque es, especialmente, útil para clausuras muy concisas. Una clausura que se pasa como el último argumento de una función puede aparecer inmediatamente después de los paréntesis. Cuando una clausura es el único argumento de una función, puedes omitir los paréntesis por completo.
let numerosOrdenados = numeros.sorted { $0 > $1 }
print(numerosOrdenados)
// Imprime "[20, 19, 12, 7]"
Objetos y clases
Usa class
seguido del nombre de la clase para crear una clase. Para declarar una propiedad de una clase, se escribe igual que al declarar una constante o variable, excepto que esta existiría en el contexto de una clase. Similarmente, la declaración de métodos y funciones se hace de la misma forma.
class Figura {
var numeroDeLados = 0
func descripcionBasica() -> String {
return "Una figura con \(numeroDeLados) lados."
}
}
Crea una instancia de una clase al poner paréntesis después del nombre de la clase. Usa sintaxis de punto para acceder a las propiedades y métodos de la instancia.
var figura = Figura()
figura.numeroDeLados = 7
var descripcionFigura = descripcionBasica()
A esta versión de la clase Figura
le hace falta algo importante: un inicializador que establezca la clase al crear una instancia. Usa init
para crear uno.
class FiguraConNombre {
var numeroDeLados: Int = 0
var nombre: String
init(nombre: String) {
self.nombre = nombre
}
func descripcionBasica() -> String {
return "Una figura con \(numeroDeLados) lados."
}
}
Observa cómo se usa self
para diferenciar la propiedad nombre
del argumento (del inicializador) nombre
. Al crear una instancia de una clase, los argumentos del inicializador se pasan como cuando se llama una función. Cada propiedad requiere que se le asigne un valor, bien sea al declararla (como con numeroDeLados
) o en el inicializador (como con nombre
).
Usa deinit
para crear un «desinicializador» (del inglés deinitializer) si necesitas llevar a cabo alguna limpieza antes de que el objeto sea «desasignado» (del inglés deallocated).
Las subclases incluyen el nombre de su súperclase después de su propio nombre, separados por una coma. No es requerimiento de las clases pasar ninguna clase base estándar a las subclases, por lo que puedes incluir u omitir una súperclase si así lo requieres.
Aquellos métodos de una subclase que redefinen la implementación de una súperclase, se marcan con override
; redefinir un método por accidente, sin usar override
, es detectado por el compilador como un error. El compilador también detecta métodos con override
que no redefinen ningún método de la súperclase.
class Cuadrado: FiguraConNombre {
var longitudLado: Double
init(longitudLado: Double, nombre: String) {
self.longitudLado = longitudLado
super.init(nombre: nombre)
numeroDeLados = 4
}
func area() -> Double {
return longitudLado * longitudLado
}
override func descripcionBasica() -> String {
return "Un cuadrado con lados de longitud \(longitudLado)."
}
}
let prueba = Cuadrado(longitudLado: 5.2, nombre: "cuadrado de prueba")
prueba.area()
prueba.descripcionBasica()
Aparte de ser simples propiedades que se pueden almacenar, las propiedades pueden tener un getter y un setter.
class TrianguloEquilatero: FiguraConNombre {
var longitudLado: Double = 0.0
init(longitudLado: Double, nombre: String) {
self.longitudLado = longitudLado
super.init(nombre: nombre)
numeroDeLados = 3
}
var perimetro: Double {
get {
return 3.0 * longitudLado
}
set {
longitudLado = newValue / 3.0
}
}
override func descripcionBasica() -> String {
return "Un triángulo equilátero con lados de longitud \(longitudLado)."
}
}
var triangulo = TrianguloEquilatero(longitudLado: 3.1, nombre: "un triángulo")
print(triangulo.perimetro)
// Imprime "9.3"
triangulo.perimetro = 9.9
print(triangulo.longitudLado)
// Imprime "3.3000000000000003"
En el setter de perimetro
, el nuevo valor tiene el nombre implícito de newValue
. Puedes proporcionar un nombre explícito en paréntesis después de set
.
Observa cómo el inicializador de la clase TrianguloEquilatero
tiene tres pasos diferentes:
- Establecer el valor de las propiedades declaradas por la subclase.
- Llamar al inicializador de la súperclase.
- Cambiar el valor de las propiedades definidas por la súperclase. Cualquier otra configuración adicional que use métodos, getters, o setters también puede llevarse a cabo en este punto.
Si no necesitas calcular la propiedad, pero igual necesitas proporcionar código que se ejecuta antes y después de establecer un nuevo valor, usa willSet
and didSet
. El código que proporciones se ejecutará cada vez que el valor cambie fuera del inicializador. Por ejemplo, la clase a continuación se asegura de que la longitud de los lados de su triángulo siempre sea la misma que la longitud de los lados de su cuadrado.
class TrianguloYCuadrado {
var triangulo: TrianguloEquilatero {
willSet {
cuadrado.longitudLado = newValue.longitudLado
}
}
var cuadrado: Cuadrado {
willSet {
triangulo.longitudLado = newValue.longitudLado
}
}
init(tamano: Double, nombre: String) {
cuadrado = Cuadrado(longitudLado: tamano, nombre: nombre)
triangulo = TrianguloEquilatero(longitudLado: tamano, nombre: nombre)
}
}
var trianguloYCuadrado = TrianguloYCuadrado(size: 10, nombre: "otra figura de prueba")
print(trianguloYCuadrado.cuadrado.longitudLado)
// Imprime "10.0"
print(trianguloYCuadrado.triangulo.longitudLado)
// Imprime "10.0"
trianguloYCuadrado.cuadrado = Cuadrado(longitudLado: 50, nombre: "cuadrado más grande")
print(trianguloYCuadrado.triangulo.longitudLado)
// Imprime "50.0"
Al trabajar con valores opcionales, puedes escribir ?
antes de operaciones como métodos, propiedades, y subscripting. Si el valor antes de ?
es nil
, todo lo que sigue a ?
es ignorado y el valor de toda la expresión es nil
. En caso contrario, se obtiene el valor opcional y todo lo que sigue a ?
opera sobre el valor obtenido. En ambos casos, el valor de toda la expresión es un valor opcional.
let cuadradoOpcional: Cuadrado? = Cuadrado(longitudLado: 2.5, nombre: "cuadrado opcional")
let longitudLado = cuadradoOpcional?.longitudLado
Enumeraciones y estructuras
Utiliza enum
para crear una enumeración. Al igual que las clases y todos los demás tipos con nombre, las enumeraciones pueden tener métodos asociados con ellas.
enum Escala: Int {
case _as = 1
case dos, tres, cuatro, cinco, seis, siete, ocho, nueve, diez
case jack, reina, rey
func descripcionBasica() -> String {
switch self {
case ._as:
return "as"
case .jack:
return "jack"
case .reina:
return "reina"
case .rey:
return "rey"
default:
return String(self.rawValue)
}
}
}
let _as = Escala._as
let valorBrutoDeAs = _as.rawValue
De manera predeterminada, Swift asigna los valores brutos comenzando por cero y aumentando en uno cada vez, pero es posible cambiar dicho comportamiento al especificar valores explícitamente. En el ejemplo anterior, a Ace
se le asigna explícitamente un valor bruto de 1
y el resto de los valores brutos se asignan en orden. También es posible utilizar cadenas de texto o números de coma flotante como el tipo bruto de una enumeración. Usa la propiedad rawValue
para acceder al valor bruto del caso de una enumeración.
Utiliza el inicializador init?(rawValue:)
para crear una instancia de una enumeración a partir de un valor bruto. Este inicializador devuelve el caso (de la enumeración) que coincida con el valor bruto o nil
si no existe tal enumeración Escala
.
if let escalaTransformada = Escala(rawValue: 3) {
let descripcionDelTres = escalaTransformada.descripcionSimple()
}
Los valores de los casos de una enumeración son valores reales, no solo otra forma de escribir sus valores brutos. De hecho, en los casos en los que no existe un valor bruto significativo, no es necesario que proporciones uno.
enum Palo {
case picas, corazones, diamantes, treboles
func descripcionBasica() -> String {
switch self {
case .picas:
return "picas"
case .corazones:
return "corazones"
case .diamantes:
return "diamantes"
case .treboles:
return "treboles"
}
}
}
let corazones = Palo.corazones
let descripcionDeCorazones = corazones.descripcionBasica()
Fíjate en las dos formas en que se referenció el caso corazones
de la enumeración: al asignar un valor a la constante corazones
, el caso Palo.corazones
se referencia por su nombre completo porque la constante no tiene un tipo especificado explícitamente. Dentro del bucle switch, el caso se referencia mediante la forma abreviada .corazones
porque ya sabemos que el valor de self
es un tipo de palo. Puedes utilizar la forma abreviada siempre que el tipo del valor ya sea conocido.
Si una enumeración tiene valores brutos, se establece que dicho valores forman parte de la declaración, lo que significa que cada instancia de un caso de enumeración en particular siempre tendrá el mismo valor bruto. Otra opción para los casos de enumeración es tener valores asociados con el caso; estos valores se determinan al crear la instancia y pueden ser diferentes para cada instancia de un caso de enumeración. Puedes ver los valores asociados como si actuaran como propiedades almacenadas de la instancia del caso de enumeración. Por ejemplo, considera el caso en el que se le pide a un servidor las horas de salida y puesta del sol. El servidor responderá con la información solicitada o responderá con una descripción de lo que haya salido mal.
enum RespuestaServidor {
case resultado(String, String)
case falla(String)
}
let logro = RespuestaServidor.resultado("6:00 am", "6:00 pm")
let error = RespuestaServidor.falla("Se han agotado los recursos.")
switch logro {
case let .resultado(amanecer, atardecer):
print("El amanecer es a las \(amanecer) y el atardecer es a las \(atardecer).")
case let .falla(mensaje):
print("Error... \(mensaje)")
}
// Imprime "El amanecer es a las 6:00 am y el atardecer es a las 6:00 pm."
Observa cómo las horas de amanecer y atardecer se extraen del valor RespuestaServidor
como parte de la comparación del valor con los casos del bucle switch.
Usa struct
para crear una estructura. Las estructuras soportan muchos de los mismos comportamientos que las clases, incluyendo métodos e inicializadores. Una de las diferencias más importantes entre estructuras y clases es que las estructuras siempre resultan siendo copiadas al usarlas a lo largo de tu código, mientras que las clases resultan siendo pasadas como referencias.
struct Carta {
var escala: Escala
var palo: Palo
func descripcionBasica() -> String {
return "El \(escala.descripcionBasica()) de \(palo.descripcionBasica())."
}
}
let tresDePicas = Carta(escala: .tres, palo: .picas)
let descripcionDelTresDePicas = tresDePicas.descripcionBasica()
Concurrencia
Usa async
para marcar una función que se ejecuta de manera asíncrona:
func obtenerIdDeUsuario(desde servidor: String) async -> Int {
if servidor == "principal" {
return 97
}
return 501
}
Para marcar el llamado a una función asíncrona, agrega la palabra clave await
después de la invocación de la función:
func obtenerNombreDeUsuario(desde servidor: String) async -> String {
let idDeUsuario = await obtenerIdDeUsuario(desde: servidor)
if idDeUsuario == 501 {
return "John Appleseed"
}
return "Visitante"
}
Usa async let
para llamar a una función asíncrona permitiéndole ejecutarse en paralelo con otro código asíncrono. Si necesitas usar el valor que devuelve, utiliza await
:
func conectarUsuario(a servidor: String) async {
async let idDeUsuario = obtenerIdDeUsuario(desde: servidor)
async let nombreDeUsuario = obtenerNombreDeUsuario(desde: servidor)
let saludo = await "Hola, \(nombreDeUsuario), ID de usuario \(idDeUsuario)"
print(saludo)
}
Usa Task
para invocar funciones asíncronas desde código sincrónico, sin tener que esperar a que estas devuelvan su valor.
Task {
await conectarUsuario(a: "principal")
}
// Imprime "Hola, Visitante, ID de usuario 97"
Protocolos y Extensiones
Usa protocol
para declarar un protocolo.
protocol ProtocoloEjemplo {
var descripcionBasica: String { get }
mutating func ajustar()
}
Las clases, enumeraciones, y estructuras pueden adoptar protocolos.
class ClaseBasica: ProtocoloEjemplo {
var descripcionBasica: String = "Una clase muy básica."
var otraPropiedad: Int = 69105
func ajustar() {
descripcionBasica += " Ahora 100% ajustada."
}
}
var a = ClaseBasica()
a.ajustar()
let descripcionDeA = a.descripcionBasica
struct EstructuraBasica: ProtocoloEjemplo {
var descripcionBasica: String = "Una estructura básica"
mutating func ajustar() {
descripcionBasica += " (ajustada)"
}
}
var b = EstructuraBasica()
b.ajustar()
let descripcionDeB = b.descripcionBasica
Nota el uso de la palabra clave mutating
en la declaración de EstructuraBasica
para marcar un método que modifica la estructura. En la declaración de ClaseBasica
no se necesita que ninguno de sus métodos esté marcado como modificador ya que los métodos de una clase siempre pueden modificar la clase misma.
Utiliza extension
para agregar funcionalidad a un tipo existente, como nuevos métodos y propiedades calculadas. Puedes usar una extensión para hacer que un tipo se ajuste a un protocolo, bien sea un tipo que se declara en otro lugar, o incluso un tipo que se importa de una biblioteca o framework.
extension Int: ProtocoloEjemplo {
var descripcionBasica: String {
return "El número \(self)"
}
mutating func ajustar() {
self += 42
}
}
print(7.descripcionBasica)
// Imprime "El número 7"
Puedes usar el nombre de un protocolo como cualquier otro tipo con nombre; por ejemplo, para crear una colección de objetos que tiene tipos diferentes, pero que todos se ajusten a un solo protocolo. Al trabajar con valores cuyo tipo es un tipo de protocolo, los métodos externos a la definición del protocolo no estarán disponibles.
let valorProtocolo: ProtocoloEjemplo = a
print(valorProtocolo.descripcionBasica)
// Imprime "Una clase muy básica. Ahora 100% ajustada."
// print(valorProtocolo.otraPropiedad) // Habilita esta línea para ver el error
Aun cuando el tipo de runtime de la variable valorProtocolo
es del tipo ClaseBasica
, el compilador lo trata como el tipo dado de ProtocoloEjemplo
. Esto significa que no puedes acceder, de manera accidental, a métodos o propiedades que la clase implementa además de su conformidad con el protocolo.
Manejo de Errores
Puedes representar errores mediante cualquier tipo que adopte el protocolo Error
.
enum ErrorImpresora: Error {
case papelAgotado
case tonerAgotado
case enLlamas
}
Usa throw
para arrojar un error y throws
para marcar una función que puede arrojar un error. Si arrojas un error en una función, la función se interrumpe inmediatamente y el código que llamó a la función se encargará de manejar el error.
func enviar(tarea: Int, impresoraDestino nombreImpresora: String) throws -> String {
if nombreImpresora == "Nunca Tiene Toner" {
throw ErrorImpresora.tonerAgotado
}
return "Tarea enviada"
}
Hay muchas maneras de manejar los errores. Una forma es usando do-catch
. Dentro del bloque do
, marcas el código que puede arrojar un error escribiendo try
delante de él. Dentro del bloque catch
, al error se le asigna automáticamente el nombre error
, a menos que le asignes un nombre diferente.
do {
let respuestaImpresora = try enviar(tarea: 1040, impresoraDestino: "Bi Sheng")
print(respuestaImpresora)
} catch {
print(error)
}
// Imprime "Tarea enviada"
Puedes usar múltiples bloques catch
que manejen errores específicos. Para esto, define un patrón después de catch
al igual que lo haces después de case
en un bucle switch.
do {
let respuestaImpresora = try enviar(tarea: 1440, impresoraDestino: "Gutenberg")
print(respuestaImpresora)
} catch ErrorImpresora.enLlamas {
print("Dejaré esto por aquí, con el resto del fuego.")
} catch let errorImpresora as ErrorImpresora {
print("Error impresora: \(errorImpresora).")
} catch {
print(error)
}
// Imprime "Tarea enviada"
Otra forma de manejar los errores es usando try?
para convertir el resultado en un opcional. Si la función arroja un error, el error específico se descarta y el resultado es nil
. De lo contrario, el resultado es un opcional que contiene el valor que devolvió la función.
let exitoImpresora = try? enviar(tarea: 1884, impresoraDestino: "Mergenthaler")
let fallaImpresora = try? enviar(tarea: 1885, impresoraDestino: "Nunca Tiene Toner")
Usa defer
para crear un bloque de código que se ejecute después de todo el código de una función, justo antes de que la función devuelva su valor. El código se ejecuta sin importar que la función arroje un error. Puedes utilizar defer
para escribir códigos de configuración y limpieza, uno al lado del otro, aun cuando estos deban ejecutarse en momentos diferentes.
var refrigeradorEstaAbierto = false
let contenidoRefrigerador = ["leche", "huevos", "sobras"]
func hayEnElRefrigerador(_ alimento: String) -> Bool {
refrigeradorEstaAbierto = true
defer {
refrigeradorEstaAbierto = false
}
let resultado = contenidoRefrigerador.contains(alimento)
return resultado
}
hayEnElRefrigerador("banana")
print(refrigeradorEstaAbierto)
// Imprime "false"
Genéricos
Escribe un nombre entre paréntesis angulares (<>
) para crear una función o tipo genérico.
func crearArray<Item>(repitiendo item: Item, numeroDeVeces: Int) -> [Item] {
var resultado: [Item] = []
for _ in 0..<numeroDeVeces {
resultado.append(item)
}
return resultado
}
crearArray(repitiendo: "toc", numeroDeVeces: 4)
También puedes crear formas genéricas de funciones y métodos, como también de clases, enumeraciones, y estructuras.
// Reimplementa el tipo opcional de la librería estándar de Swift
enum OptionalValue<Wrapped> {
case none
case some(Wrapped)
}
var posibleEntero: OptionalValue<Int> = .none
posibleEntero = .some(100)
Utiliza where
justo antes del cuerpo de la función para especificar una lista de requerimientos; por ejemplo, para requerir el tipo para implementar un protocolo, para requerir que dos tipos sean el mismo, o para requerir que una clase tenga una súperclase en particular.
func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool
where T.Element: Equatable, T.Element == U.Element
{
for lhsItem in lhs {
for rhsItem in rhs {
if lhsItem == rhsItem {
return true
}
}
}
return false
}
anyCommonElements([1, 2, 3], [3])
Escribir <T: Equatable>
es igual a escribir <T> ... where T: Equatable
.