La biblioteca estándar de Swift revela un hecho llamativo: no solo los tipos básicos como Int, Double y Bool, sino también String, Array, Dictionary y Set son structs. Lo que en otros lenguajes serían clases son tipos por valor en Swift. String es una clase en Java, y en Python todo es una referencia a un objeto.
No es casualidad. Swift estableció durante su diseño que “los tipos por valor son la opción predeterminada” y declaró oficialmente esta dirección en las famosas sesiones de WWDC 2015 “Protocol-Oriented Programming” y “Building Better Apps with Value Types”. Este artículo resume por qué Swift usa valores en lugar de referencias como opción predeterminada y cómo esa decisión afectó a todo el lenguaje.
Esta es la tercera parte de la serie sobre la filosofía de Swift. Las diferencias sintácticas entre clases y structs se tratan en otro artículo; aquí nos centramos en por qué se diseñó esa diferencia.
Los problemas crónicos de un mundo basado en referencias
Para entender la prioridad de los tipos por valor, primero debemos examinar los problemas de un mundo donde los tipos por referencia son la opción predeterminada.
La esencia de los tipos por referencia es compartir. Aunque copies una variable, solo hay un objeto y ambas variables apuntan a él. Si compartir es intencionado, es una funcionalidad; si no, se convierte en una fuente de errores. El patrón clásico es este.
// Supongamos un tipo por referencia (clase)
let settings = defaultSettings
settings.fontSize = 20 // No pensaba modificar la configuración predeterminada
// defaultSettings.fontSizey 20ocurrió esto
Creías haberlo copiado, pero lo estabas compartiendo. Lo desagradable de estos errores es que el síntoma y la causa están muy alejados. El código que dañó el valor puede estar a varios archivos de distancia, así que depurar consiste en “seguir todas las referencias a este objeto”.
Los desarrolladores de Objective-C conocían bien este problema y tradicionalmente se protegían contra él. Declaraban las propiedades NSString con copy, separaban las variantes mutables de NSArray (NSMutableArray) de las inmutables y añadían copias defensivas como práctica habitual. Eran parches que compensaban con disciplina del desarrollador los problemas de tener referencias como opción predeterminada.
El equipo de Swift lo planteó así: si hay que protegerse de un problema mediante convenciones cada vez, ¿no estará equivocado el valor predeterminado del lenguaje?
Lo que protegen los tipos por valor — razonamiento local
La propiedad esencial de un tipo por valor es que al copiarlo se obtiene una entidad realmente independiente.
var a = [1, 2, 3]
var b = a
b.append(4)
// asigue [1, 2, 3]
Esto garantiza más que comodidad: permite el razonamiento local. Al pasar un array a una función, si es un tipo por valor no necesitas preocuparte de que la función modifique tu array a escondidas. Puedes comprender por completo el estado de tu variable leyendo solo tu bloque de código. En un mundo de tipos por referencia había que pensar, en todo el programa, quién conservaba un objeto y cuándo podía modificarlo; los tipos por valor reducen ese alcance a la función que tienes delante.
Esto conecta directamente con la filosofía Safe tratada en la primera parte. Igual que los opcionales detectan en tiempo de compilación el “olvido de nil”, los tipos por valor eliminan a nivel de tipos la categoría de errores del “compartido no intencionado”. Además, un struct declarado con let es realmente inmutable. En una instancia de clase, let solo significa que la referencia no cambia; su contenido sí puede cambiar.
Esta propiedad ha ganado aún más valor con el tiempo. En un entorno multihilo, una carrera de datos aparece cuando “varios hilos comparten la misma memoria”, pero los tipos por valor no se comparten de entrada, así que desaparece la premisa de la carrera. Es natural que Swift Concurrency considere los tipos por valor ejemplos representativos de tipos Sendable capaces de cruzar límites entre hilos. Una decisión de diseño de 2014 fue recuperada por el modelo de concurrencia de 2021.
“¿Copiar no es caro?” — La respuesta: Copy-on-Write
La primera objeción a priorizar los tipos por valor siempre es el rendimiento. Si un array de 100.000 elementos se copiara entero cada vez que se pasa a una función, ¿no sería demasiado costoso?
La respuesta de Swift es Copy-on-Write (CoW). Colecciones estándar como Array, Dictionary, Set y String comparten el almacenamiento interno al asignarse y solo realizan una copia real cuando una de las partes se modifica. Semánticamente son valores completos cuyas modificaciones no se hacen visibles entre sí, con un coste similar al de las referencias cuando solo se leen.
var a = hugeArray // No hay copia; almacenamiento compartido
let x = a[0] // Sigue sin haber copia
a.append(1) // La primera copia ocurre en este momento
Lo importante es que CoW es una técnica de implementación de biblioteca, no una funcionalidad del lenguaje. Está incorporada en las colecciones estándar, pero no se añade automáticamente a los structs que creamos. Si necesitas un tipo por valor personalizado que contenga datos grandes, debes implementarlo con isKnownUniquelyReferenced. Este detalle de implementación se explica en otro artículo sobre CoW.
También existe una ventaja de rendimiento en la dirección opuesta. Los structs pequeños pueden vivir en la pila sin asignación en el heap y no requieren conteo de referencias, por lo que son incluso más baratos que las clases. Por eso tipos como CGPoint son structs. Así que la intuición de que “los tipos por valor son lentos” suele funcionar al revés en Swift.
Cómo vivir sin herencia — protocolos y composición
Priorizar los tipos por valor tiene un coste: los structs no admiten herencia. Como es difícil implementar polimorfismo parcial sin referencias, ¿cómo conseguimos reutilización de código y polimorfismo?
La respuesta de Swift es la programación orientada a protocolos (POP). Declaramos las interfaces comunes como protocolos, colocamos las implementaciones comunes en extensiones de protocolos y combinamos capacidades haciendo que los tipos adopten varios protocolos. La herencia es una estructura vertical en la que todo se recibe de un único padre; adoptar protocolos es una estructura horizontal que permite elegir y añadir las capacidades necesarias.
struct Player: Codable, Equatable, Comparable {
let name: String
let score: Int
static func < (lhs: Self, rhs: Self) -> Bool {
lhs.score < rhs.score
}
}
Este struct no hereda nada, pero admite conversión a JSON, comparación de igualdad y ordenación. Además, el compilador sintetiza automáticamente Codable y Equatable. La combinación de tipos por valor y protocolos reemplaza la mayoría de los usos prácticos de la herencia.
Por eso priorizar los tipos por valor y la programación orientada a protocolos forman un conjunto. No fue casualidad que las dos sesiones se presentaran juntas en WWDC 2015. “Structs y protocolos en lugar de herencia de clases” es la combinación predeterminada que propone Swift, y toda la biblioteca estándar está construida así. POP se trata con detalle en otro artículo.
Entonces, ¿cuándo usar clases?
Priorizar los tipos por valor no significa “no uses clases”. La regla exacta es “struct por defecto; clase cuando haya motivos para necesitar referencias”. Hay aproximadamente tres casos.
Cuando importa la identidad. Las cosas que son “entidades distintas aunque sus valores coincidan”, como conexiones de base de datos, vistas en pantalla y descriptores de archivo, encajan naturalmente con referencias. Dos conexiones con la misma configuración no son la misma conexión.
Cuando compartir es el objetivo. Los modelos compartidos que deben observar varias pantallas y los gestores que deben existir como una única instancia global de la app aprovechan el carácter compartido de las referencias.
Cuando se necesita gestionar el ciclo de vida. Se usa una clase si deinit debe liberar recursos o si hay que interactuar con frameworks de Objective-C como UIKit.
La documentación oficial de Apple recomienda lo mismo: usar structs y enums por defecto y elegir clases cuando se cumplan estas condiciones. En la práctica, el código de las apps de la era SwiftUI converge en vistas como structs, datos de estado como structs y unos pocos modelos por referencia (clases @Observable). La filosofía de que los valores son la opción predeterminada y las referencias la excepción llega hasta el nivel de los frameworks de UI.
Resumen
- Que la biblioteca estándar de Swift sea casi toda structs es una decisión de diseño. La opción predeterminada es un tipo por valor.
- El objetivo es eliminar a nivel de tipos los errores lejanos causados por el compartido no intencionado en lenguajes basados en referencias.
- Los tipos por valor protegen el razonamiento local, y esta propiedad reapareció en Sendable dentro de Swift Concurrency.
- Copy-on-Write en las colecciones estándar y la asignación en la pila para structs pequeños responden a la objeción del rendimiento.
- La programación orientada a protocolos cubre el vacío de la herencia, y ambas se diseñaron como un conjunto.
- Las clases se redefinieron como la herramienta para cuando se necesitan identidad, compartición o gestión del ciclo de vida.
Hasta aquí hemos visto en la serie sobre la filosofía de Swift la seguridad (parte 1), la curva de aprendizaje (parte 2) y los tipos por valor (parte 3). En la próxima parte trataremos Swift Evolution, el proceso mediante el cual estas filosofías llegan al lenguaje. Es el recorrido desde que una propuesta sintáctica recibe un número SE-XXXX hasta que entra en Swift.
Artículos recomendados
- [Filosofía de Swift #4] ¿Qué es SE-0296? Cómo nace la sintaxis de Swift: guía completa de Swift Evolution
- [Swift intermedio #1] Guía completa de Swift ARC: elige weak o unowned según la relación de ciclo de vida
- [Swift intermedio #2] Desde los fundamentos de genéricos (Generics) de Swift hasta su uso práctico: cómo
elimina a la vez la duplicación y los riesgos

![Imagen de portada de [Filosofía de Swift #3] Por qué Swift usa structs: tipos por valor](/assets/images/posts/c6e5417c-1bad-4913-bdff-974f972e1a73/1.jpg)