← Volver al blog

struct — tipo por valor

La diferencia clave entre struct y class en Swift: tipos por valor vs tipos por referencia. Entiende cuándo usar cada uno y por qué es fundamental para tu código.

Clases y estructuras son los bloques con los que modelas los datos de tu app. En Swift ambas son muy poderosas, pero tienen una diferencia fundamental que cambia cómo se comportan: las estructuras son tipos por valor y las clases son tipos por referencia. Entender esto es clave para escribir código correcto y predecible.

struct — tipo por valor

Cuando asignas una estructura a otra variable o la pasas a una función, se crea una copia independiente. Modificar la copia no afecta al original. Este comportamiento predecible es una de las razones por las que Swift moderno prefiere struct sobre class en la mayoría de los casos.

struct Punto {
    var x: Double
    var y: Double
}

var origen = Punto(x: 0, y: 0)
var destino = origen   // se crea una COPIA

destino.x = 10
destino.y = 10

print(origen.x)   // 0.0 — no cambió
print(destino.x)  // 10.0 — solo cambió la copia

class — tipo por referencia

Cuando asignas una clase a otra variable, ambas apuntan al mismo objeto en memoria. Modificar uno afecta al otro. Esto puede generar bugs difíciles de rastrear si no lo tienes claro.

class Contador {
    var valor: Int = 0
}

let contadorA = Contador()
let contadorB = contadorA  // apuntan al MISMO objeto

contadorB.valor = 100

print(contadorA.valor)  // 100 — ¡cambió porque es el mismo objeto!
print(contadorB.valor)  // 100

Propiedades

Tanto structs como clases pueden tener propiedades almacenadas (guardan un valor) y propiedades calculadas (calculan un valor a partir de otros).

struct Rectangulo {
    // Propiedades almacenadas
    var ancho: Double
    var alto: Double

    // Propiedad calculada — no almacena, calcula
    var area: Double {
        ancho * alto
    }

    var perimetro: Double {
        2 * (ancho + alto)
    }
}

let rect = Rectangulo(ancho: 5, alto: 3)
print(rect.area)       // 15.0
print(rect.perimetro)  // 16.0

Métodos

Las funciones definidas dentro de un tipo se llaman métodos. En structs, los métodos que modifican propiedades deben marcarse con mutating.

struct Pila {
    private var elementos: [Int] = []

    // mutating porque modifica la struct
    mutating func agregar(_ elemento: Int) {
        elementos.append(elemento)
    }

    mutating func quitar() -> Int? {
        elementos.popLast()
    }

    var estaVacia: Bool {
        elementos.isEmpty
    }

    var cantidad: Int {
        elementos.count
    }
}

var pila = Pila()
pila.agregar(10)
pila.agregar(20)
pila.agregar(30)
print(pila.cantidad)  // 3
print(pila.quitar()!) // 30
print(pila.cantidad)  // 2

Inicializadores

Los structs reciben un inicializador automático con todas sus propiedades (memberwise initializer). Las clases necesitan uno explícito si las propiedades no tienen valor por defecto.

// Struct — inicializador gratis
struct Producto {
    var nombre: String
    var precio: Double
    var disponible: Bool = true  // valor por defecto
}

let p1 = Producto(nombre: "Libro", precio: 15.99)              // disponible = true
let p2 = Producto(nombre: "Curso", precio: 49.99, disponible: false)

// Class — necesita init explícito
class Sesion {
    var token: String
    var expiracion: Date

    init(token: String, expiracion: Date) {
        self.token     = token
        self.expiracion = expiracion
    }
}

Herencia — exclusiva de class

Solo las clases soportan herencia. Una clase puede heredar propiedades y métodos de otra y sobreescribirlos con override. Los structs no heredan — en su lugar se usan protocolos.

class Animal {
    var nombre: String

    init(nombre: String) {
        self.nombre = nombre
    }

    func sonido() -> String {
        "..."
    }
}

class Perro: Animal {
    override func sonido() -> String {
        "Guau"
    }
}

class Gato: Animal {
    override func sonido() -> String {
        "Miau"
    }
}

let animales: [Animal] = [Perro(nombre: "Rex"), Gato(nombre: "Luna")]
for animal in animales {
    print("\(animal.nombre): \(animal.sonido())")
}
// Rex: Guau
// Luna: Miau

¿Cuándo usar struct y cuándo class?

La guía oficial de Apple y la comunidad Swift son claras: empieza siempre con struct. Cambia a class solo cuando necesites alguna de estas características:

  • Herencia (necesitas que un tipo extienda a otro)
  • Compartir estado mutable entre múltiples partes del código de forma intencionada
  • Interoperabilidad con Objective-C o APIs que requieran clases
  • Gestión del ciclo de vida del objeto con deinit

En SwiftUI, por ejemplo, los modelos de datos son structs casi siempre. Las clases aparecen en los @Observable o ObservableObject que gestionan estado compartido.

// ✅ Modelo de datos — usar struct
struct Articulo {
    let id: UUID
    var titulo: String
    var contenido: String
    var publicado: Bool
}

// ✅ Estado compartido en SwiftUI — usar class con @Observable
import Observation

@Observable
class AppState {
    var usuarioActivo = false
    var contadorNotificaciones = 0
}

Resumen

  • struct: tipo por valor, crea copias, comportamiento predecible — úsalo por defecto.
  • class: tipo por referencia, comparte el mismo objeto — úsalo cuando necesitas herencia o estado compartido.
  • Propiedades calculadas: derivan su valor de otras propiedades, sin almacenar.
  • mutating: requerido en métodos de struct que modifican propiedades.
  • Memberwise initializer: los structs lo reciben gratis; las clases necesitan init explícito.
  • Herencia: exclusiva de clases; los structs usan protocolos en su lugar.

Con esto completas los fundamentos de Swift moderno. El siguiente paso es explorar protocolos, generics y concurrencia con async/await — temas que cubriremos en posts intermedios de la serie.