Funciones y switch
Al terminar: Escribir funciones con etiquetas de argumento y valores por defecto, y usar switch exhaustivo.
Al terminar tendrás la regla de los puntos escrita con un switch sobre el marcador, puntos(para:contra:), y titular(golesLocal:golesVisitante:), que convierte un marcador en una frase con pattern matching. Por el camino ves lo que Swift añade a una función: las etiquetas de argumento.
Importa porque en Swift la etiqueta forma parte del nombre de la función, y porque el switch obliga a cubrir todos los casos: el compilador te avisa de cada hueco antes de ejecutar. Esas dos ideas aparecen en cada línea de Swift que leerás.
Temas: etiquetas de argumento; switch exhaustivo, pattern matching y where. Los valores por defecto y inout van en la lección siguiente.
El fichero de hoy
Hoy trabajas sobre todo en Puntos.swift, el fichero donde escribiste puntosPor en La regla de los puntos y tu primer test. Ábrelo ahora: en el Project navigator de Xcode, Sources → Torneo → Puntos.swift (o open -a Xcode ~/Marcador-Swift/Torneo/Sources/Torneo/Puntos.swift desde la Terminal). Verás puntosPor con su if. Al final de la lección tendrá dos funciones más, y las ampliarás de una en una.
Los ejemplos sueltos de la teoría y el ejercicio los escribes en tu Playground de Xcode, el que creaste en Tu primer paquete de Swift. Reutiliza el mismo y borra lo anterior antes de pegar otro ejemplo: dos funciones con el mismo nombre y las mismas etiquetas chocan.
Funciones: la etiqueta es parte del nombre
Cada parámetro tiene dos nombres: la etiqueta, que escribes al llamar, y el nombre, que se usa dentro del cuerpo.
| Cómo se declara | Cómo se llama | Qué pasa |
|---|---|---|
func f(goles: Int) | f(goles: 2) | Un solo nombre: etiqueta y nombre son el mismo |
func f(para goles: Int) | f(para: 2) | Etiqueta para, nombre interno goles |
func f(_ goles: Int) | f(2) | _ quita la etiqueta |
El nombre completo de una función incluye las etiquetas: puntosPor(golesAFavor:golesEnContra:). Por eso Swift permite tener dos funciones con el mismo nombre y etiquetas distintas.
Dos diferencias con Kotlin, donde pasabas argumentos con nombre en Funciones y parámetros. En Swift las etiquetas son obligatorias (no puedes escribir solo los valores, sin etiquetas, salvo que la función use _), y van en el orden declarado. Si declaras puntos(para:contra:) y la llamas puntos(contra: 1, para: 2), el compilador dice argument 'para' must precede argument 'contra'; y si la llamas puntos(2, 1), dice missing argument labels 'para:contra:' in call. En Kotlin, con nombres, el orden da igual.
switch: exhaustivo, sin caída y con patrones
switch es el when de Swift, con cuatro diferencias:
- Exhaustivo siempre. Un
whende Kotlin que solo ejecuta cosas, sin usar su valor, no exigeelse; unswitchde Swift exige cubrir todos los casos, uses o no su valor. Con un número o un texto, eso pide undefault. - Sin caída. Cada
casetermina solo y no sigue con el de abajo (en otros lenguajes hay que cortar a mano conbreak). Aquí no hace falta. - Patrones. Un
casepuede comparar un valor, un rango (0...45), una tupla, y ligar valores a nombres conlet. whereañade una condición a un patrón.
Una tupla es un grupo de valores entre paréntesis, como (2, 1). Un switch sobre (golesLocal, golesVisitante) compara los dos valores a la vez, y el comodín _ significa «cualquier valor, no me interesa». Una tupla también puede ser el tipo de un parámetro, como marcador: (Int, Int), y se lee por posición: marcador.0 es el primer valor y marcador.1, el segundo.
// `_ a`: sin etiqueta al llamar. `con b`: etiqueta con, nombre interno b.
func sumar(_ a: Int, con b: Int) -> Int { a + b }
// Una tupla como parámetro: `marcador.0` y `marcador.1` son sus dos valores.
func golesTotales(_ marcador: (Int, Int)) -> Int { marcador.0 + marcador.1 }
func tramo(minuto: Int) -> String {
switch minuto { // compara minuto con cada case, en orden
case 0...45: "primer tiempo" // un rango: de 0 a 45, los dos incluidos
case 46...90: "segundo tiempo" // sin break: ningún case cae al siguiente
default: "prórroga" // obligatorio: un Int tiene infinitos valores
}
}
print(sumar(1, con: 2)) // 3
print(tramo(minuto: 47)) // segundo tiempo
print(golesTotales((2, 1))) // 3Lo que imprime:
3
segundo tiempo
3Aquí el switch es el cuerpo de la función y cada case es una sola expresión sin return: el valor del case elegido es el resultado. Escribir return en cada case también vale, y es la forma que verás en puntos(para:contra:). tramo reparte el minuto en tres casos, como el when de puntosPor que escribiste en Condiciones: if y when.
Dos piezas más del switch, con otros datos: si la temperatura sube, se mantiene o baja. case let (h, a) liga cada valor de la tupla a un nombre nuevo (aquí h y a), y where añade una condición que ese case debe cumplir además de encajar.
// Etiquetas hoy: y ayer:; dentro se usan gradosHoy y gradosAyer.
func tendencia(hoy gradosHoy: Int, ayer gradosAyer: Int) -> String {
switch (gradosHoy, gradosAyer) { // la tupla junta los dos valores
case let (h, a) where h > a: // let liga cada valor a un nombre; where pone la condición
return "sube"
case let (h, a) where h == a: // solo se mira si el case anterior no encajó
return "igual"
default: // el compilador no razona los where: hace falta
return "baja"
}
}
print(tendencia(hoy: 20, ayer: 18)) // sube
print(tendencia(hoy: 10, ayer: 12)) // bajacase (let h, let a) where … es la misma forma con el let dentro de cada parte.
Kotlin frente a Swift
| Kotlin (tu código de Kotlin) | Swift |
|---|---|
fun puntosPor(…): Int = when { … } | func puntosPor(…) -> Int { … } |
when { a > b -> 3; a == b -> 1; else -> 0 } | switch (a, b) { case let (x, y) where x > y: … default: … } |
else -> … | default: … |
| Argumentos con nombre, en cualquier orden | Etiquetas obligatorias, en el orden declarado |
in 0..45 -> … | case 0...45: … |
when { … } sin sujeto | No existe: se usa switch sobre una tupla, como aquí, o if / else if |
Reto · Ejercicio: el arranque del partido
En tu Playground, escribe una función, descripcion, que reciba el minuto y los goles de los dos equipos juntos en una tupla, y elija la respuesta con un solo switch que mire a la vez el minuto y el total de goles. Debe devolver «Nada que contar» si el minuto está entre 0 y 9 y no hay goles, «Arranque con goles» si el minuto está entre 0 y 9 y los hay, y «Va el partido» en cualquier otro caso. Imprime la descripción del minuto 3 con 0-0, la del minuto 3 con 1-0 y la del minuto 60 con 2-1. Lo que debe salir:
Nada que contar
Arranque con goles
Va el partidoVer solución
func descripcion(minuto: Int, goles: (Int, Int)) -> String {
switch (minuto, goles.0 + goles.1) { // una tupla: el minuto y el total
case (0...9, 0): "Nada que contar" // minuto de 0 a 9 y ningún gol
case (0...9, _): "Arranque con goles" // minuto de 0 a 9 y cualquier total
default: "Va el partido"
}
}
print(descripcion(minuto: 3, goles: (0, 0)))
print(descripcion(minuto: 3, goles: (1, 0)))
print(descripcion(minuto: 60, goles: (2, 1)))El orden de los case importa: el segundo solo se alcanza si el primero no encajó, y el default recoge el resto.
Paso · puntos(para:contra:) y titular
Objetivo: ampliar Puntos.swift con las etiquetas y el switch, y ampliar main.swift. Al terminar, xcrun swift run torneo-cli imprime siete titulares de marcadores.
Abre una Terminal y escribe cd ~/Marcador-Swift/Torneo. Puntos.swift ya existe y lo tienes abierto; main.swift está en Sources → torneo-cli. Vas en dos tramos, y compilas y ejecutas al terminar cada uno.
Tramo 1 · puntos(para:contra:): etiquetas y switch
Piezas nuevas:
func puntos(para golesAFavor: Int, contra golesEnContra: Int): cada parámetro con etiqueta y nombre distintos.switch (a, b) { … }ycase let (x, y) where x > y:: ellety elwheredel ejemplotendenciade arriba, ahora con los goles.return valoren cadacase, o solovalor: las dos formas valen.
puntosPor(golesAFavor:golesEnContra:) se conserva, pero ahora delega en puntos(para:contra:), así que los tests de la lección anterior siguen pasando.
Intenta escribirlo tú: sustituye el contenido de Puntos.swift por dos funciones públicas.
puntos, que recibe los goles a favor con la etiquetaparay los goles en contra con la etiquetacontra, y devuelve 3 si se gana, 1 si se empata y 0 si se pierde, mirando los dos valores a la vez.puntosPor, la de siempre (mismas etiquetas,golesAFavorygolesEnContra), que ahora se limita a llamar apuntos.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// Se conserva con la firma de siempre, pero ahora delega en `puntos(para:contra:)`.
public func puntosPor(golesAFavor: Int, golesEnContra: Int) -> Int {
puntos(para: golesAFavor, contra: golesEnContra)
}
// Etiqueta y nombre distintos: se llama `puntos(para: 2, contra: 1)`
// y dentro se usan `golesAFavor` y `golesEnContra`.
public func puntos(para golesAFavor: Int, contra golesEnContra: Int) -> Int {
// `switch` sobre una tupla: compara los dos valores a la vez.
switch (golesAFavor, golesEnContra) {
case let (aFavor, enContra) where aFavor > enContra: // `let` liga los dos valores
return 3
case let (aFavor, enContra) where aFavor == enContra: // `where` añade una condición
return 1
default: // obligatorio: el compilador no razona sobre los `where`
return 0
}
}Ejecuta xcrun swift run torneo-cli: la salida es la de siempre, porque puntosPor da lo mismo que antes.
Ahora provoca el error del switch: borra el default de puntos(para:contra:) (la línea default: y su return 0) y ejecuta xcrun swift build. El compilador se detiene con error: switch must be exhaustive y la nota add missing case: '(_, _)': no razona sobre los where, así que no sabe que «mayor» e «igual» dejan solo «menor». Devuelve el default a su sitio.
Tramo 2 · titular: el switch con patrones
Los siete casos de titular(golesLocal:golesVisitante:), en este orden (se aplica el primer case que encaja) y con estos textos exactos (la lección siguiente te hace copiar unos tests que los comprueban):
| Caso | Texto que devuelve |
|---|---|
Marcador (0, 0) | Sin goles |
| Empate (mismos goles) | Empate a N, con los goles de cada equipo en lugar de N |
| Gana el local sin recibir goles | El local gana sin recibir goles |
| Gana el visitante sin recibir goles | El visitante gana sin recibir goles |
| Diferencia de tres o más goles | Goleada |
| Gana el local | Gana el local |
| Cualquier otro | Gana el visitante |
Piezas nuevas:
case (0, 0):ycase (_, 0):: comparar la tupla con valores concretos;_es el comodín.abs(local - visitante): valor absoluto, una función de la librería estándar.for (local, visitante) in [(0, 0), (2, 2)] { … }: recorre una lista de tuplas y separa cada una en dos nombres. Las listas se ven a fondo en Colecciones y key paths.
Las tres piezas con otros datos: dónde cae un punto (x, y) en un plano.
func dondeEsta(x: Int, y: Int) -> String {
switch (x, y) {
case (0, 0): // los dos valores tienen que ser 0
"En el origen"
case (_, 0): // x, cualquiera (_); y, exactamente 0
"Sobre el eje X"
case (0, _): // x, 0; y, cualquiera
"Sobre el eje Y"
case let (x, y) where abs(x) == abs(y): // abs quita el signo: abs(-3) es 3
"En una diagonal"
default: // todo lo que no encajó arriba
"En otro sitio"
}
}
// Cada vuelta toma una tupla de la lista y la separa en x e y.
for (x, y) in [(0, 0), (4, 0), (-3, 3), (1, 5)] {
print("(\(x), \(y)): \(dondeEsta(x: x, y: y))")
}
// (0, 0): En el origen · (4, 0): Sobre el eje X · (-3, 3): En una diagonal · (1, 5): En otro sitioIntenta escribirlo tú, al final de Puntos.swift: titular, una función pública que recibe los goles con las etiquetas golesLocal y golesVisitante y devuelve el texto de la tabla de arriba, mirando los dos valores a la vez y en ese orden.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// Cada `case` es una sola expresión sin `return`: su valor es el resultado.
// Se aplica el primer `case` que encaja, por eso el orden importa.
public func titular(golesLocal: Int, golesVisitante: Int) -> String {
switch (golesLocal, golesVisitante) {
case (0, 0):
"Sin goles"
case (let local, let visitante) where local == visitante:
"Empate a \(local)"
case (_, 0): // `_`: el local marcó lo que sea y el visitante, 0
"El local gana sin recibir goles"
case (0, _):
"El visitante gana sin recibir goles"
case let (local, visitante) where abs(local - visitante) >= 3:
"Goleada"
case let (local, visitante) where local > visitante:
"Gana el local"
default:
"Gana el visitante"
}
}En main.swift, después del doble bucle de marcadores (el que imprime … puntos para el local) y antes del comentario // Una lista de tres jugadores, intenta escribir tú un fragmento que recorra estos siete marcadores, 0-0, 2-2, 3-0, 0-1, 5-1, 2-1 y 1-3, e imprima cada uno con su titular. Lo que debes ver:
0-0: Sin goles
2-2: Empate a 2
3-0: El local gana sin recibir goles
0-1: El visitante gana sin recibir goles
5-1: Goleada
2-1: Gana el local
1-3: Gana el visitanteIntenta primero. Cuando termines, compara con la solución.Mostrar el código
// Una lista de tuplas: cada vuelta separa la tupla en `local` y `visitante`.
for (local, visitante) in [(0, 0), (2, 2), (3, 0), (0, 1), (5, 1), (2, 1), (1, 3)] {
print("\(local)-\(visitante): \(titular(golesLocal: local, golesVisitante: visitante))")
}Ejecútalo: deben salir siete líneas, de 0-0: Sin goles a 1-3: Gana el visitante.
Punto de control
- Desde
~/Marcador-Swift/Torneo,xcrun swift buildtermina conBuild complete!. - Desde
~/Marcador-Swift/Torneo,xcrun swift run torneo-cliimprime, además de lo de siempre, los siete titulares (de0-0: Sin golesa1-3: Gana el visitante, con5-1: Goleadaentre ellos), antes del bloque de opcionales. - Desde
~/Marcador-Swift/Torneo,xcrun swift testtermina conTest run with 11 tests in 2 suites passed(ignora la líneaExecuted 0 testsde XCTest): los 4 dePuntosTestsy los 7 deJugadorTests, que siguen pasando porquepuntosPorda lo mismo. - Quitaste el
defaultdepuntos(para:contra:), leísteswitch must be exhaustivey lo devolviste a su sitio. - Sabes decir por qué
titulardaSin golespara0-0y noEmpate a 0, y por qué3-0daEl local gana sin recibir golesy noGoleada.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Una función se declara
func puntos(para golesAFavor: Int, contra golesEnContra: Int) -> Int. ¿Cómo se llama?2.Una función se declara
func puntos(para golesAFavor: Int, contra golesEnContra: Int) -> Inty la llamaspuntos(contra: 1, para: 2). ¿Qué ocurre?3.Escribes un
switch (a, b)sobre una tupla de dosInt, con uncase … wherepara «amayor queb», otro para «aigual ab» y sindefault. ¿Qué pasa?4.En
titular(golesLocal: 3, golesVisitante: 0), ¿por qué devuelve «El local gana sin recibir goles» y no «Goleada»?5.En
case (_, 0):de unswitchsobre una tupla de goles, ¿qué significa el_?