wandres.dev
OPCIONALES A FONDO · el arte de la ausencia

Optional es un enum, no una anotación

La interrogación de Swift es azúcar sobre un enum genérico de dos casos. Qué declara de verdad la biblioteca estándar, qué añade el compilador encima y por qué verlo así cambia la lectura de todo el lenguaje.

⏱ 16 min

Casi todos aprendemos los opcionales como una regla de sintaxis: pon una interrogación y el valor podrá faltar. Es cierto, y es inútil, porque no explica nada de lo que viene después. La verdad es más simple y mucho más potente: Optional es un enum corriente, escrito en Swift, con dos casos y sin una gota de magia. Todo lo demás —el nil, la interrogación, el ??, el encadenamiento— es azúcar que el compilador deposita encima de ese enum.

🎯 Al terminar esta lección sabrás
  • Leer la declaración real de Optional en la biblioteca estándar.
  • Manejar Optional.some y Optional.none sin ninguna abreviatura.
  • Enumerar el azúcar que añade el compilador y a qué se traduce cada pieza.
  • Entender que nil no es un puntero nulo, sino un caso de un tipo.

La declaración que hay debajo

Si abres la biblioteca estándar y buscas Optional, no encuentras una palabra clave ni un tipo primitivo del compilador. Encuentras esto, que podrías haber escrito tú después de la lección de enums:

// Biblioteca estándar, simplificado
@frozen
public enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

extension Optional: ExpressibleByNilLiteral {
    public init(nilLiteral: ()) { self = .none }
}

Dos casos. El caso none no lleva nada: es la ausencia. El caso some lleva un valor asociado del tipo genérico Wrapped, que es el tipo envuelto. Un Optional de Int no es un Int que a veces está vacío: es una caja que o contiene un Int o está marcada como vacía, y la etiqueta que dice cuál de las dos cosas es forma parte del valor.

Puedes escribirlo entero, sin ninguna abreviatura, y comprobar que es exactamente lo mismo que la sintaxis que usas a diario:

let a: Optional<Int> = Optional.some(42)
let b: Optional<Int> = Optional.none
let c: Int? = 42       // idéntico a la línea de a
let d: Int? = nil      // idéntico a la línea de b

print(a == c, b == d)  // true true

Y como es un enum normal, se recorre con switch como cualquier otro enum, sin herramientas especiales:

switch c {
case .none:
    print("no hay nada")
case .some(let valor):
    print("hay \(valor)")
}
ℹ️
nil no es un puntero nulo

En C, NULL es la dirección cero: un valor del mismo tipo que cualquier puntero, colado donde debería haber un objeto. En Swift, nil no tiene tipo propio ni representación fija; es un literal que cada tipo que adopta ExpressibleByNilLiteral interpreta a su manera, y en el caso de Optional significa el caso none. Por eso nil no se puede asignar a un Int no opcional: no existe un Int que valga nil. La ausencia vive en el tipo que envuelve, nunca dentro del tipo envuelto.

El azúcar, pieza a pieza

Sobre ese enum de diez líneas, el compilador añade una capa de notación. Conviene saber exactamente qué es azúcar y a qué se traduce, porque cuando el compilador te dé un error hablará del enum, no del azúcar.

🍊

La interrogación pospuesta

Int? es notación abreviada de Optional<Int>. Son el mismo tipo escrito de dos maneras, y puedes intercambiarlas en cualquier firma.

🍋

El literal nil

nil se resuelve como Optional.none a través de ExpressibleByNilLiteral. No es una palabra clave con poderes: es una conformidad de protocolo.

🍏

La promoción implícita

Donde se espera un Int? puedes pasar un Int a secas: el compilador inserta el .some por ti. Es la única conversión implícita de tipos que Swift se permite.

🍇

El patrón con interrogación

En un switch, case let x? es azúcar de case .some(let x). Sirve en cualquier posición de patrón, incluidas tuplas y bucles.

La promoción implícita merece un segundo de atención, porque es la que hace que los opcionales resulten cómodos y también la que produce diagnósticos desconcertantes cuando envuelves algo sin querer:

func registrar(_ apodo: String?) { print(apodo as Any) }

registrar("jefa")                 // el compilador escribe Optional.some por ti
let doble: Int?? = Int?.some(3)   // un opcional dentro de otro opcional: dos etiquetas

Incluso el operador ?? es una función genérica declarada en Swift, no una construcción del lenguaje. Su firma revela algo importante: el valor por defecto es un @autoclosure, así que solo se evalúa si de verdad hace falta.

public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T

Queda una pieza que casi nadie sitúa bien: el Tipo! tampoco declara un tipo distinto. Desde Swift 4.2 esa admiración es un atributo de la declaración, no parte del tipo. Lo que declaras sigue siendo un Optional corriente; lo único que cambia es que el compilador tiene permiso para insertar un desenvolvimiento forzado allí donde el contexto exija el valor desnudo.

var vista: UIView!      // el tipo de la propiedad sigue siendo UIView?
let copia = vista       // se infiere UIView?, no UIView: la marca no se propaga
flowchart TD
S[Sintaxis Wrapped con interrogacion] --> T[Tipo Optional de Wrapped]
L[Literal nil] --> N[Caso none: ausencia etiquetada]
V[Valor sin envolver] --> M[Caso some con el valor dentro]
T --> N
T --> M
style T fill:#cba6f7,color:#11111b
style N fill:#f9e2af,color:#11111b
style M fill:#a6e3a1,color:#11111b

Consecuencias de que sea un enum de verdad

Que Optional sea un tipo normal tiene efectos que no se explican si lo ves como una anotación. El primero: anida. Un Int?? tiene tres estados distinguibles —hay un tres, hay una ausencia interior, no hay nada— y eso no es un accidente, es aritmética de tipos. Aparece de forma natural al buscar en un diccionario cuyos valores ya son opcionales.

El segundo: hereda conformidades de forma condicional. Optional es Equatable si Wrapped lo es, y lo mismo con Hashable. Por eso puedes comparar dos opcionales directamente, o meterlos en un Set, sin escribir nada.

El tercero, y el más revelador, es que su coste en memoria es el de un enum con carga, sujeto a la misma optimización que cualquier otro: si el tipo envuelto deja patrones de bits sin usar, la etiqueta se codifica ahí y el opcional no ocupa ni un byte de más.

MemoryLayout<Int>.size       // 8
MemoryLayout<Int?>.size      // 9: un Int usa los 64 bits, hace falta etiqueta aparte
MemoryLayout<Bool>.size      // 1
MemoryLayout<Bool?>.size     // 1: a un Bool le sobran valores, la etiqueta cabe dentro
MemoryLayout<String?>.size   // igual que String: aprovecha bits libres del buffer

Y un cuarto efecto que solo se entiende desde el enum: como none es el nombre real de un caso, colisiona con cualquier enum tuyo que también tenga un caso llamado none. El compilador resuelve la ambigüedad a favor del opcional y te avisa, pero el aviso solo tiene sentido si sabes que hay dos casos compitiendo por el mismo nombre corto.

enum Prioridad { case none, alta }

let ambiguo: Prioridad? = .none          // el compilador asume Optional.none y avisa
let interior: Prioridad? = .some(.none)  // presente, y dentro la prioridad none
let vacio: Prioridad? = nil              // sin ambigüedad posible
💡
Cuando el compilador te hable de some, te está hablando del enum

Los mensajes de error mencionan value of optional type y sugieren ? o ! porque el verificador razona sobre Optional<Wrapped>, no sobre tu notación. Si traduces mentalmente el error al enum —“me has dado la caja donde esperaba el contenido”— casi todos los diagnósticos de opcionales se vuelven obvios de inmediato.

Desmitificar el azúcar es lo que te deja razonar

Interioriza esto y las cuatro lecciones que siguen serán consecuencias, no reglas nuevas que memorizar. El null de otros lenguajes es un agujero en el sistema de tipos: un impostor admitido en cualquier hueco, invisible para el verificador, que convierte cada tipo en una promesa a medias. Optional no es un parche sobre esa idea, es su negación: la ausencia deja de ser un valor que se cuela en todas partes y pasa a ser un caso de un tipo concreto, con etiqueta, con nombre y con exhaustividad. La consecuencia práctica es enorme: cualquier cosa que sepas hacer con enums —hacer switch, extraer valores asociados, escribir extensiones, componer con genéricos— la sabes hacer ya con opcionales, y todo lo que Swift añade encima es notación para que ese trabajo sea breve. El azúcar es comodidad, no semántica. Quien solo aprende el azúcar acumula recetas y se queda paralizado en el primer caso raro, un opcional doble o un patrón anidado. Quien ve el enum debajo deriva la respuesta desde primeros principios cada vez. Esa es la diferencia entre usar Swift y entenderlo.

⚔️ Quítale el azúcar a los opcionales
  1. Reescribe cinco declaraciones opcionales de tu código con la sintaxis larga Optional<T> y Optional.some, y comprueba que todo sigue compilando.
  2. Declara un Int??, construye sus tres estados posibles y distínguelos con un solo switch anidado.
  3. Mide con MemoryLayout el tamaño de Int?, Bool?, String? y Double?, y explica cada resultado a partir de los bits sobrantes del tipo envuelto.
  4. Escribe una extension sobre Optional con una propiedad estaVacio y observa que no necesitas ningún permiso especial para extender el tipo.
  5. Busca en la biblioteca estándar la declaración del operador ?? y razona qué pasaría si el segundo parámetro no fuese un @autoclosure.