Swift de Cero a Experto #11: Opcionales y Optional Chaining
El antídoto de Swift al error de mil millones de dólares. Optional es solo un enum — y nil, if let, guard let, ??, ! y el optional chaining compilan a preguntar en qué case estás.
En el artículo anterior recorrimos una class de nacimiento a muerte — init garantizando un objeto válido, deinit devolviendo lo que pidió prestado. Hasta vimos un failable initializer (init?) entregarte una instancia opcional, y prometimos contar la historia completa aquí. Esta es esa historia.
Los opcionales son la respuesta de Swift al error de mil millones de dólares — así llamó Tony Hoare a la referencia nula que inventó en 1965, causante de incontables crashes desde entonces. El arreglo de Swift no es un truco ingenioso de runtime. Es un tipo. Un valor de tipo Int siempre es un entero. Un valor de tipo Int? es o bien un entero o bien nada — y el sistema de tipos no te deja olvidar la segunda posibilidad.
Un opcional no es una característica especial del lenguaje atornillada a los tipos. Es solo un enum con dos cases — uno que guarda un valor, otro que no. Todo lo demás es sintaxis sobre esa única idea.
nil: la ausencia de un valor
Ya conociste los opcionales sin nombrarlos. Un failable initializer retorna uno:
let possibleNumber = "123"let convertedNumber = Int(possibleNumber)// convertedNumber es de tipo "optional Int", o "Int?"Int(_:) no puede prometer éxito — "123" se convierte limpiamente, pero "hello" no — así que retorna un Int? en vez de un Int. Para representar “sin valor”, asigna nil a un opcional:
var serverResponseCode: Int? = 404serverResponseCode = nil// serverResponseCode ahora no contiene valorSi declaras una variable opcional sin darle valor, queda automáticamente en nil:
var surveyAnswer: String?// surveyAnswer queda automáticamente en nilBajo el capó: Optional es un enum
Aquí está la parte por la que existe esta serie. Optional no es sintaxis mágica — es un enum genérico común y corriente de la librería estándar. <Wrapped> es un tipo de marcador de posición que se rellena con lo que sea que envuelvas — Int? es Optional<Int>, así que Wrapped se convierte en Int. (Los genéricos tienen su propio artículo más adelante; aquí solo necesitas saber que Optional<Int> significa “un Optional que contiene un Int”.) Su declaración es esencialmente:
enum Optional<Wrapped> { case none // la ausencia de un valor — esto es lo que significa `nil` case some(Wrapped) // la presencia de un valor, guardado como Wrapped}El ? que escribes es puro azúcar sintáctico: Int? es exactamente Optional<Int>. nil es exactamente Optional.none. Asignar un valor real lo envuelve en .some. Las dos formas son intercambiables:
let shortForm: Int? = Int("42")let longForm: Optional<Int> = Int("42")
let number: Int? = Optional.some(42)let noNumber: Int? = Optional.noneprint(noNumber == nil) // trueUna vez que ves esto, cada operador de este artículo deja de ser misterioso. Desempaquetar un opcional es literalmente el pattern matching de enums del #6 — preguntar “¿estoy en el case .some, y si sí, cuál es el valor asociado?”. if let, guard let, ?? y ! son todos formas ergonómicas de hacer esa misma pregunta.

Forced unwrapping: el ! que puede crashear
Si estás seguro de que un opcional contiene un valor, puedes meter la mano y sacarlo con un signo de exclamación al final — forced unwrapping:
let possibleNumber = "123"let convertedNumber = Int(possibleNumber)
if convertedNumber != nil { print("convertedNumber has an integer value of \(convertedNumber!).")}// convertedNumber has an integer value of 123.convertedNumber! dice “sé que esto no es nil — dame el Int de adentro”. En términos de enum, afirma que estás en el case .some y retorna el valor asociado.
Optional binding: if let y guard let
La forma if let desempaqueta en una constante nueva que solo existe dentro del cuerpo del if:
let possibleNumber = "123"
if let actualNumber = Int(possibleNumber) { print("The string \"\(possibleNumber)\" has an integer value of \(actualNumber)")} else { print("The string \"\(possibleNumber)\" couldn't be converted to an integer")}// The string "123" has an integer value of 123Si Int(possibleNumber) es .some(value), actualNumber queda ligado a ese valor y corre la rama del if. Si es .none, corre el else. El actualNumber desempaquetado es un Int normal — sin ?, sin !.
Cuando la constante desempaquetada compartiría el nombre del opcional, puedes usar la forma abreviada: solo escribe if let nombre sin =:
if let convertedNumber { // convertedNumber ahora es un Int no opcional, con el mismo nombre que el opcional print(convertedNumber)}Puedes ligar varios opcionales (y agregar cláusulas booleanas) en un mismo if, separados por comas. Todo el conjunto tiene éxito solo si cada binding lo tiene:
if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 { print("\(firstNumber) < \(secondNumber) < 100")}// 4 < 42 < 100guard let: ligar para el resto del scope
if let liga dentro de su propio bloquecito. A menudo quieres lo contrario — desempaquetar una vez al inicio de una función y usar el valor durante el resto del cuerpo. Para eso es guard let:
func greet(_ name: String?) { guard let name else { print("No name provided") return } // name es un String no opcional desde aquí hasta el final de la función print("Hello, \(name)!")}Un guard exige que su condición sea verdadera para continuar. Si el binding falla, corre el bloque else y debe salir del scope actual (return, break, throw, etc.). La recompensa: el name desempaquetado permanece en scope para todo lo que viene después del guard, sin indentación extra. Este es el patrón de salida temprana del #5, aplicado a opcionales.
El operador nil-coalescing ??
let defaultColorName = "red"var userDefinedColorName: String? // queda en nil por defecto
var colorNameToUse = userDefinedColorName ?? defaultColorName// userDefinedColorName es nil, así que colorNameToUse queda en "red"El lado derecho b debe coincidir con el tipo del a desempaquetado. Aquí userDefinedColorName es String? y defaultColorName es String, así que el resultado es un String no opcional.
Como ?? puede recibir otro opcional a su derecha, puedes encadenarlo para expresar “intenta esto, luego aquello, luego cae al default”:
let imagePaths = ["star": "/glyphs/star.png"]let defaultImagePath = "/images/default.png"
let shapePath = imagePaths["cir"] ?? imagePaths["squ"] ?? defaultImagePathprint(shapePath)// /images/default.png — ambos lookups fallaron, así que gana el default finalImplicitly unwrapped optionals
El caso que lo motiva: un valor que es nil durante una ventana breve del setup, y luego permanentemente no nil. Escribir ? y desempaquetarlo en cada acceso sería puro ruido.
let possibleString: String? = "An optional string."let forcedString: String = possibleString! // requiere un ! explícito
let assumedString: String! = "An implicitly unwrapped optional string."let implicitString: String = assumedString // sin ! — desempaquetado automáticamenteAún puedes tratar un implicitly unwrapped optional como un opcional normal — compararlo contra nil, ligarlo con if let. Pero cada uso simple fuerza un unwrap tras bambalinas:
if assumedString != nil { print(assumedString!)}
if let definiteString = assumedString { print(definiteString)}Optional chaining: consultar a través de nil
Pasamos el capítulo desempaquetando un solo opcional. Pero los modelos reales se anidan — una Person tiene una Residence opcional, que tiene una Address opcional, que tiene un street opcional. El optional chaining te deja bajar a través de todos ellos en una sola expresión, fallando con gracia en el instante en que cualquier eslabón es nil.
El optional chaining es el hermano seguro del forced unwrapping. Donde ! hace trap con nil, ? retorna nil. Empieza con dos clases simples:
class Person { var residence: Residence?}
class Residence { var numberOfRooms = 1}Una Person recién creada tiene residence == nil (los opcionales quedan en nil por defecto). Forzar a través de ella crashea:
let john = Person()let roomCount = john.residence!.numberOfRooms// esto dispara un error en runtime — residence es nilCambia el ! por ? y la llamada falla con gracia en su lugar, retornando nil:
if let roomCount = john.residence?.numberOfRooms { print("John's residence has \(roomCount) room(s).")} else { print("Unable to retrieve the number of rooms.")}// Unable to retrieve the number of rooms.
Encadenar varios niveles de profundidad
Puedes encadenar tan profundo como llegue tu modelo. Agrega una Address con un street opcional:
class Residence { var rooms: [Room] = [] var numberOfRooms: Int { rooms.count } subscript(i: Int) -> Room { rooms[i] } var address: Address?}
class Room { let name: String init(name: String) { self.name = name }}
class Address { var street: String?}Ahora baja a través de dos eslabones opcionales de una vez:
if let johnsStreet = john.residence?.address?.street { print("John's street name is \(johnsStreet).")} else { print("Unable to retrieve the address.")}// Unable to retrieve the address.Encadenar sobre subscripts y métodos
El ? va antes de los corchetes de un subscript — siempre sigue inmediatamente a la parte opcional de la expresión:
if let firstRoomName = john.residence?[0].name { print("The first room name is \(firstRoomName).")} else { print("Unable to retrieve the first room name.")}// Unable to retrieve the first room name.Llamar un método a través de una cadena funciona incluso para métodos que no retornan nada. Un método que retorna Void se vuelve Void? a través de una cadena — y comparar eso contra nil te dice si la llamada realmente ocurrió:
if john.residence?.printNumberOfRooms() != nil { print("It was possible to print the number of rooms.")} else { print("It was not possible to print the number of rooms.")}// It was not possible to print the number of rooms.El mismo truco funciona para asignar a través de una cadena. La asignación retorna Void?; si la cadena era nil, ni siquiera se evalúa nada del lado derecho:
let someAddress = Address()john.residence?.address = someAddress// residence es nil, así que someAddress nunca se asigna — y la cadena no hace nada en silencioEl forced unwrapping dice “esto nunca es nil — crashea si me equivoco”. El optional chaining dice “esto podría ser nil — y está bien, solo devuélveme nil”. La misma dualidad ?/!, hasta el fondo.
Recapitulación

- Optional — un valor que está presente (
.some) o ausente (.none/nil); se escribeT?, que es exactamenteOptional<T> - Bajo el capó —
Optionales solo un enum con dos cases; cada unwrap es pattern matching de enum - nil — la ausencia de un valor, no un puntero nulo; solo los opcionales pueden ser
nil - Forced unwrapping (
!) — saca el valor pero hace trap si esnil; una promesa que puedes romper - Optional binding (
if let/guard let) — desempaqueta de forma segura en una constante no opcional;guard letla mantiene en scope para el resto del cuerpo - Nil-coalescing (
??) — desempaqueta-o-default; el default es perezoso (@autoclosure) y hace short-circuit - Implicitly unwrapped (
T!) — un opcional desempaquetado automáticamente en cada uso; igual crashea si esnil - Optional chaining (
?) — consulta a través de opcionales anidados; el resultado es siempre opcional y la cadena se aplana y falla con gracia
Lo que viene
En el próximo artículo exploramos el Error Handling — throws, try, do/catch y Result. Veremos cómo Swift traza una línea nítida entre una ausencia esperada de un valor (un opcional) y un fallo esperado con una razón (un error lanzado), y por qué try? convierte calladamente uno en el otro.
Nos vemos la próxima semana.
El error de mil millones de dólares no fue null en sí — fue hacer que toda referencia fuera nullable por defecto. El arreglo de Swift es hacer la ausencia visible en el tipo. Una vez que interiorizas “un opcional es solo un enum”, nil deja de dar miedo y empieza a ser honesto.
Referencias
Relacionados
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #10: Herencia e Inicialización
Subclassing, overriding y super. Designated vs convenience initializers, two-phase initialization, failable y required init, y deinit — el ciclo de vida completo de una class, explicado en memoria.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #9: Propiedades, métodos y subscripts
Stored vs computed properties, observers, lazy, static. Cómo las propiedades definen el layout en memoria y por qué computed = zero storage.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #8: Structs vs Classes — la decisión que define tu app
Value semantics vs reference semantics, static vs dynamic dispatch, y por qué Apple recomienda structs por defecto. El artículo que cambia cómo piensas en Swift.