any: la caja que puede contener a cualquiera
Un existencial es un tipo real cuyos valores llevan dentro un conformante cualquiera y, junto a él, la maquinaria para operar sobre un tipo desconocido. Heterogeneidad, despacho dinámico, apertura implícita del existencial y las capacidades que el borrado de tipo te arrebata. Por qué la palabra any dejó de ser opcional.
Si some es un tipo concreto con la cara tapada, any es lo contrario: un tipo nuevo, distinto de todos sus conformantes, cuyos valores son cajas capaces de albergar hoy un Circulo y mañana un Cuadrado. Esa caja no guarda solo el valor: guarda también las instrucciones para copiarlo, destruirlo y llamar a sus métodos sin saber qué es. A cambio de esa flexibilidad, el tipo concreto desaparece del sistema de tipos —eso es el borrado de tipo— y con él desaparecen ciertas capacidades. La palabra any, obligatoria desde Swift 5.6, existe precisamente para que ese intercambio quede escrito.
- Leer
any Pcomo un tipo distinto de sus conformantes, con su propia representación en memoria. - Reconocer la heterogeneidad y la decisión en ejecución como el territorio propio del existencial.
- Identificar qué se pierde con el borrado de tipo: requisitos de
Selfy tipos asociados en entrada. - Recuperar el tipo concreto mediante la apertura implícita del existencial al pasarlo a un genérico.
Una caja con etiqueta
Un existencial es un contenedor uniforme. Todos los valores de tipo any Figura tienen la misma forma y el mismo tamaño, independientemente de qué haya dentro.
protocol Figura { var area: Double { get } }
struct Circulo: Figura { let radio: Double; var area: Double { 3.14159 * radio * radio } }
struct Rect: Figura { let l: Double; var area: Double { l * l } }
var caja: any Figura = Circulo(radio: 2) // dentro: un Circulo
caja = Rect(l: 3) // ahora: un Rect, misma variable
print(caja.area) // 9.0
Que la variable acepte la reasignación es el rasgo definitorio. Un genérico o un opaco quedan atados a un único tipo; el existencial no. Para lograrlo, la caja lleva tres cosas: el valor —o un puntero a él—, un puntero a los metadatos del tipo real y un puntero a la tabla de testigos del protocolo, que es donde vive la implementación concreta de cada requisito. Cuando escribes el acceso a la propiedad, el programa consulta la tabla en ejecución y salta a la implementación correcta. Eso es despacho dinámico.
flowchart TB E[Valor de tipo any Figura] --> B[Buffer con el valor o puntero al monton] E --> M[Puntero a metadatos del tipo real] E --> W[Puntero a la tabla de testigos de Figura] W --> A[Implementacion de area para el tipo real] M --> V[Como copiar destruir y medir el valor] A --> R[Llamada indirecta en ejecucion] style E fill:#89b4fa,color:#11111b style W fill:#cba6f7,color:#11111b style R fill:#f38ba8,color:#11111b
Heterogeneidad: el territorio propio de any
Hay una cosa que ningún genérico puede hacer y que el existencial resuelve sin esfuerzo: guardar juntos valores de tipos distintos.
let escena: [any Figura] = [Circulo(radio: 1), Rect(l: 2), Circulo(radio: 5)]
let total = escena.reduce(0) { $0 + $1.area }
Un array genérico es homogéneo por definición: todos sus elementos son del mismo tipo. Para mezclar hace falta un tipo común que los abarque a todos, y ese es exactamente el existencial. La misma necesidad aparece cuando el tipo se decide con datos que solo existen en ejecución —un plugin registrado, una respuesta descodificada, una fábrica que elige según configuración— o cuando un tipo se refiere a sí mismo de forma recursiva y no puede fijar el tipo del hijo en compilación.
final class Nodo {
var contenido: any Figura // el tipo varia por nodo
var siguiente: Nodo?
init(_ c: any Figura) { contenido = c; siguiente = nil }
}
Ese nodo es imposible de escribir con un opaco: una propiedad opaca queda atada a un único tipo, y aquí el tipo debe poder diferir entre un nodo y el siguiente.
Otra propiedad que solo tiene el existencial: puedes preguntarle qué lleva dentro y recuperar el tipo concreto con una conversión condicional.
for f in escena {
if let c = f as? Circulo { print("radio", c.radio) }
}
Lo que la caja te quita
El borrado de tipo no es gratis en expresividad. Todo requisito cuya firma dependa de la identidad del tipo concreto queda inaccesible sobre la caja cerrada.
let a: any Equatable = 1
let b: any Equatable = "uno"
// a == b // error: los operandos podrian ser de tipos distintos
La misma lógica alcanza a los tipos asociados en posición de entrada. Sobre una colección existencial puedes leer, porque el elemento sale; no puedes insertar, porque el elemento entra y su tipo es desconocido para quien llama.
let c: any Collection = [1, 2, 3]
print(c.count) // bien: count no menciona ningun tipo asociado
// let primero = c.first // el tipo del resultado seria Any, poco util
La cura parcial son los tipos asociados primarios, que permiten fijar en la propia caja el tipo asociado más importante y devolverle utilidad:
let c: any Collection<Int> = [1, 2, 3]
let primero: Int? = c.first // ahora Element es Int, y el resultado es tipado
La cura completa es abrir el existencial: pasarlo a una función genérica. Desde Swift 5.7 esa apertura es implícita, y dentro del genérico el tipo vuelve a ser único y conocido.
func describir<T: Figura>(_ f: T) -> String { "area \(f.area)" }
let x: any Figura = Circulo(radio: 1)
print(describir(x)) // el compilador abre la caja y fija T al tipo real
Una fuente clásica de confusión: el tipo any Figura no conforma al protocolo Figura, salvo en casos especiales que el compilador trata aparte, como any Error. Por eso una caja no puede pasarse directamente donde se exige un genérico restringido sin la apertura implícita, ni satisfacer un requisito con tipos asociados. El existencial contiene conformantes; no es uno de ellos.
El coste que no se ve
El precio del existencial tiene tres partidas, y ninguna aparece en el código fuente.
La primera es la representación. La caja tiene un buffer pequeño de tres palabras; si el valor cabe ahí y es trivialmente movible, se guarda dentro. Si no cabe, se asigna en el montón y la caja pasa a guardar un puntero. Un struct de cuatro campos cruzando ese umbral cambia una copia de registros por una asignación de memoria.
La segunda es el despacho indirecto. Cada llamada pasa por la tabla de testigos, y el optimizador no puede insertar en línea a través de ella ni razonar sobre el cuerpo del método. En un bucle apretado eso puede pesar más que la propia llamada.
La tercera es el conteo de referencias. Copiar un existencial que apunta al montón implica retener; destruirlo, liberar. Recorrer un array grande de cajas genera tráfico de ARC que un array homogéneo no genera.
Conviene además recordar que la caja tiene tamaño fijo con independencia de lo que guarde: todos los valores de tipo any Figura miden lo mismo, y esa uniformidad es justo lo que permite meterlos en un mismo array. El precio es que ese tamaño fijo debe ser lo bastante generoso para el caso común y lo bastante rígido para no serlo nunca del todo.
struct Grande: Figura { var a, b, c, d, e: Double; var area: Double { a } }
let dentro: any Figura = Circulo(radio: 1) // cabe en el buffer: sin asignacion
let fuera: any Figura = Grande(a: 1, b: 2, c: 3, d: 4, e: 5) // al monton
Nada de esto convierte al existencial en un error de diseño. Convertirlo en la opción por defecto sí lo es. Un puñado de cajas en la frontera de un sistema —donde el mundo exterior introduce variedad genuina— es exactamente el uso para el que la herramienta existe; miles de cajas en el interior de un bucle de cálculo son una decisión de arquitectura tomada por descuido.
Lo que de verdad hace any es una operación filosófica antes que técnica: coge la pregunta “¿de qué tipo es este valor y cómo se comporta?” —una pregunta que normalmente nace y muere durante la compilación— y la transforma en un dato manipulable en ejecución, un puñado de punteros que puedes guardar, copiar, meter en un array y pasar a otra función. Eso es una reificación: comportamiento estático convertido en estructura de datos. Y explica de un plumazo tanto su poder como su precio. El poder es que un mismo trozo de código máquina puede operar sobre valores cuyos tipos ni siquiera existían cuando se compiló, porque el código ya no habla con el tipo, habla con la tabla. El precio es que toda la información que el sistema de tipos dejó de llevar debe viajar ahora en memoria, ocupando espacio, exigiendo indirecciones y bloqueando optimizaciones que dependían de conocer el tipo. Fíjate en la simetría exacta con some: ambos ocultan el tipo concreto al lector, y por eso se parecen tanto en la sintaxis; pero uno lo oculta después de que el compilador lo haya usado para generar código óptimo, y el otro lo oculta también al compilador, obligándolo a generar código que funcione con cualquiera. La misma opacidad aparente, dos momentos distintos, dos mundos de rendimiento. Por eso Swift 5.6 hizo obligatoria la palabra any: durante años el nombre desnudo del protocolo significaba silenciosamente esta segunda cosa, y un programa podía estar pagando cajas, asignaciones y saltos indirectos en su ruta crítica sin que en el código apareciera una sola marca de que aquello estaba ocurriendo. La palabra clave no añadió capacidad: añadió honestidad.
- Construye un array de tres tipos distintos que conformen a un mismo protocolo y recórrelo sumando una propiedad; comprueba que ningún array genérico podría contenerlos.
- Declara una constante
any Collectionsin tipo asociado primario y otra con él; compara qué miembros te deja usar cada una. - Escribe una función genérica restringida y pásale un valor existencial: acabas de provocar una apertura implícita, descríbela con tus palabras.
- Intenta comparar dos valores
any Equatabley después resuelve la misma comparación pasando ambos a una función genérica; explica por qué una vía compila y la otra no. - Crea un struct de una sola palabra y otro de seis, guárdalos en sendos existenciales y razona cuál de los dos exige una asignación en el montón.