¿struct o class? La guía definitiva para elegir correctamente en Swift
Arturo Rivas Arias
Cuando empezamos a programar en Swift es habitual aprender las diferencias entre struct y class: unas son tipos por valor, otras por referencia; unas admiten herencia y otras no. Sin embargo, la verdadera dificultad no está en conocer esas diferencias, sino en saber cuál utilizar en cada situación.
Apple ha actualizado recientemente su documentación para dejar clara una recomendación que lleva años aplicando en sus propios frameworks: utiliza estructuras por defecto y recurre a las clases únicamente cuando realmente las necesites.
Esta filosofía está presente en la biblioteca estándar de Swift, en Foundation y también en SwiftUI, donde gran parte de las APIs están diseñadas alrededor de tipos por valor.
Empieza siempre por una struct
Las estructuras han evolucionado mucho desde los primeros lenguajes orientados a objetos. En Swift pueden tener propiedades almacenadas, propiedades calculadas, inicializadores, métodos, extensiones e incluso adoptar protocolos con implementaciones por defecto.
En la práctica, ofrecen prácticamente todo lo necesario para modelar datos sin asumir los inconvenientes de compartir referencias entre distintas partes de la aplicación.
struct Product {
let id: UUID
var name: String
var stock: Int
mutating func sell(units: Int) {
stock -= units
}
}
Este tipo es completamente independiente. Si lo asignamos a otra variable, estaremos trabajando con una copia.
var keyboard = Product(
id: UUID(),
name: "Magic Keyboard",
stock: 25
)
var warehouse = keyboard
warehouse.sell(units: 5)
print(keyboard.stock) // 25
print(warehouse.stock) // 20
Cada instancia mantiene su propio estado, lo que facilita enormemente razonar sobre el código.
Las ventajas de los tipos por valor
El mayor beneficio de las estructuras no es el rendimiento, sino la predictibilidad.
Cuando una función recibe una struct, sabemos que cualquier modificación permanecerá dentro de esa copia, salvo que decidamos devolver un nuevo valor o modificarlo mediante inout.
Esto reduce considerablemente los efectos secundarios inesperados.
En aplicaciones grandes, donde una misma entidad puede ser utilizada desde decenas de lugares distintos, este comportamiento hace que los errores sean mucho más fáciles de localizar.
Además, esta filosofía encaja perfectamente con SwiftUI, donde las vistas son estructuras efímeras que describen un estado y pueden recrearse continuamente sin afectar al resto de la aplicación.
¿Cuándo utilizar una class?
Las clases siguen siendo imprescindibles en determinados escenarios.
Apple destaca principalmente tres casos.
Interoperabilidad con Objective-C
Muchos frameworks históricos siguen basándose en Objective-C.
Si necesitas heredar de una clase del sistema o trabajar con APIs que esperan objetos Objective-C, no tendrás alternativa.
Es el caso de clases como UIViewController, NSObject o muchos componentes de AppKit y UIKit.
Cuando la identidad importa
Una clase no solo almacena datos. También representa una identidad.
Dos instancias pueden contener exactamente los mismos valores y seguir siendo objetos completamente diferentes.
final class DownloadSession {
var progress: Double = 0
}
let first = DownloadSession()
let second = DownloadSession()
print(first === second) // false
El operador === compara si ambas referencias apuntan exactamente al mismo objeto en memoria.
Esta característica es fundamental para representar recursos compartidos, como una conexión de red, un gestor Bluetooth, un reproductor multimedia o una conexión a una base de datos.
En estos casos queremos que todas las partes de la aplicación trabajen sobre el mismo objeto.
Compartir referencias también tiene un precio
Precisamente porque todas las referencias apuntan al mismo objeto, cualquier modificación será visible desde cualquier otro lugar.
final class UserSettings {
var darkMode = false
}
let shared = UserSettings()
let screenA = shared
let screenB = shared
screenA.darkMode = true
print(screenB.darkMode) // true
Este comportamiento puede ser exactamente lo que buscamos… o convertirse en una fuente constante de errores.
Apple insiste en tratar la identidad con cuidado.
Cuantas más partes de la aplicación compartan una misma instancia, más difícil será entender quién está modificando su estado y cuándo ocurre.
La identidad puede vivir fuera de la aplicación
Existe otro caso interesante que menciona Apple.
Imaginemos que nuestra aplicación obtiene información desde un servidor.
struct Customer: Decodable {
let id: UUID
var name: String
var city: String
}
Aunque cada cliente tenga un identificador único, esa identidad pertenece al servidor, no a nuestra aplicación.
Nosotros simplemente estamos modelando los datos recibidos.
En este escenario una estructura sigue siendo la mejor elección, ya que modificar una copia local nunca alterará accidentalmente el estado compartido del sistema remoto.
Protocolos antes que herencia
Otro de los consejos de Apple es priorizar la composición mediante protocolos frente a la herencia entre clases.
En lugar de construir largas jerarquías de clases, suele resultar más flexible definir capacidades independientes.
protocol Searchable {
func matches(_ text: String) -> Bool
}
struct Book: Searchable {
let title: String
func matches(_ text: String) -> Bool {
title.localizedCaseInsensitiveContains(text)
}
}
struct Movie: Searchable {
let title: String
func matches(_ text: String) -> Bool {
title.localizedCaseInsensitiveContains(text)
}
}
Este enfoque funciona con estructuras, clases e incluso enumeraciones, evitando muchas de las limitaciones de la herencia clásica.
¿Y qué ocurre con Observation y SwiftUI?
Aquí suele aparecer una aparente contradicción.
Apple recomienda utilizar estructuras por defecto, pero el nuevo sistema @Observable funciona sobre clases.
En realidad ambas recomendaciones son compatibles.
Los modelos que representan datos (Product, Order, Customer…) continúan siendo excelentes candidatos para convertirse en estructuras.
En cambio, los objetos cuya misión es coordinar estado compartido entre varias vistas sí necesitan identidad, por lo que una clase observable resulta la opción adecuada.
Es habitual encontrar aplicaciones donde el modelo de dominio está compuesto casi exclusivamente por estructuras, mientras que los view models, controladores de estado o servicios son clases.
Conclusión
La pregunta ya no debería ser “¿struct o class?”, sino “¿realmente necesito identidad compartida?”.
Si la respuesta es no, probablemente una struct sea la elección correcta.
Reserva las clases para aquellos casos donde varias partes de la aplicación deban trabajar sobre la misma instancia, cuando necesites interoperar con Objective-C o cuando estés representando recursos cuyo estado debe mantenerse sincronizado.
Seguir esta recomendación no solo acerca tu código al estilo utilizado por Apple, sino que también hace que sea más sencillo de mantener, más fácil de razonar y menos propenso a errores derivados del estado compartido.