Swift de Cero a Experto #9: Propiedades, métodos y subscripts
Stored vs computed properties, observers, lazy, static. Cómo las propiedades definen el layout en memoria y por qué computed = zero storage.
En el artículo anterior descubrimos que structs viven en el stack y classes en el heap. Hoy entramos al tejido conectivo de ambos: propiedades, métodos y subscripts — las piezas que definen qué sabe tu tipo, qué puede hacer, y cómo accedes a sus datos.
No todas las propiedades cuestan lo mismo. Una stored property ocupa espacio real en el layout de memoria de tu tipo. Una computed property no ocupa ni un byte — es una función disfrazada de propiedad. Entender esa diferencia es la clave para diseñar tipos eficientes.
Una stored property ocupa bytes reales en cada instancia. Una computed property ocupa cero — es simplemente una función con sintaxis de propiedad.

Stored properties: el dato real
struct FixedLengthRange { var firstValue: Int // 8 bytes — stored let length: Int // 8 bytes — stored (inmutable)}
// MemoryLayout<FixedLengthRange>.size == 16 bytes (8 + 8)var range = FixedLengthRange(firstValue: 0, length: 3)range.firstValue = 6 // OK — var// range.length = 4 // ERROR — let es inmutableRecuerda del artículo #8: si el struct se asigna a una constante let, ninguna de sus propiedades puede cambiar — ni siquiera las var:
let fixedRange = FixedLengthRange(firstValue: 0, length: 3)// fixedRange.firstValue = 6 // ERROR — todo el struct es inmutableCon classes es diferente — let solo protege la referencia, no el objeto:
class SomeClass { var value = 42}let instance = SomeClass()instance.value = 99 // OK — let protege la referencia, no las propiedadesLazy stored properties: alocación diferida
class DataManager { lazy var importer = DataImporter() var data: [String] = []}
let manager = DataManager()// importer NO se ha creado todavía — zero costo hasta que lo necesitesmanager.data.append("Some data")
print(manager.importer.filename)// AHORA se crea DataImporter — solo cuando realmente lo necesitas
¿Cuándo usar lazy?
- El valor depende de factores externos que no se conocen hasta después del
init - El cálculo inicial es costoso y quizás nunca se necesite
- La propiedad depende de
self(que no existe durante la inicialización)
Computed properties: funciones disfrazadas
struct Rect { var origin: Point var size: Size
var center: Point { get { Point( x: origin.x + (size.width / 2), y: origin.y + (size.height / 2) ) } set { origin.x = newValue.x - (size.width / 2) origin.y = newValue.y - (size.height / 2) } }}
var square = Rect( origin: Point(x: 0, y: 0), size: Size(width: 10, height: 10))print(square.center) // Point(x: 5, y: 5)square.center = Point(x: 15, y: 15)print(square.origin) // Point(x: 10, y: 10)center no ocupa espacio — se calcula cada vez. Si solo tienes getter, puedes simplificar:
struct Cuboid { var width = 0.0, height = 0.0, depth = 0.0
var volume: Double { // read-only computed width * height * depth }}- Stored property → ocupa bytes reales en cada instancia. Determina
MemoryLayout.size - Computed property → zero bytes. Es una función que el compilador puede inlinear
- Lazy stored → ocupa los bytes del valor más una pequeña cantidad de estado extra para rastrear si ya se inicializó (definido por la implementación)
- Truco del compilador: si una computed property es simple, el compilador la hace inline (inline = el compilador pega el cuerpo de la función directamente en el lugar de la llamada, eliminando el costo de la llamada a función) — equivalente a acceder directamente al campo

Property observers: willSet y didSet
class StepCounter { var totalSteps: Int = 0 { willSet(newTotalSteps) { print("A punto de cambiar a \(newTotalSteps)") } didSet { if totalSteps > oldValue { print("Agregaste \(totalSteps - oldValue) pasos") } } }}
let counter = StepCounter()counter.totalSteps = 200// "A punto de cambiar a 200"// "Agregaste 200 pasos"counter.totalSteps = 360// "A punto de cambiar a 360"// "Agregaste 160 pasos"willSetrecibe el nuevo valor como parámetro (default:newValue)didSettiene acceso al valor anterior (default:oldValue)- Puedes usar uno, ambos, o ninguno
Los observers también funcionan en propiedades heredadas — puedes agregar willSet/didSet a una propiedad que heredas de una superclass.
Type properties: static y class
struct AudioChannel { static let thresholdLevel = 10 static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 { didSet { if currentLevel > AudioChannel.thresholdLevel { currentLevel = AudioChannel.thresholdLevel } if currentLevel > AudioChannel.maxInputLevelForAllChannels { AudioChannel.maxInputLevelForAllChannels = currentLevel } } }}
var leftChannel = AudioChannel()leftChannel.currentLevel = 7print(AudioChannel.maxInputLevelForAllChannels) // 7 — compartidoProperty wrappers: reutiliza la lógica
Una nota antes del código: 0...100 es un ClosedRange<Int> — un rango que incluye ambos extremos; .lowerBound y .upperBound devuelven su primer y último valor.
@propertyWrapperstruct Clamped { var wrappedValue: Int { didSet { wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) } } let range: ClosedRange<Int>
init(wrappedValue: Int, _ range: ClosedRange<Int>) { self.range = range self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) }}
struct Player { @Clamped(0...100) var health: Int = 100 @Clamped(0...999) var score: Int = 0}
var player = Player()player.health = 150 // se clampea a 100player.health = -10 // se clampea a 0wrappedValue es el nombre obligatorio que Swift busca: leer o escribir player.health pasa por el wrappedValue del wrapper, y @Clamped(0...100) pasa 0...100 al init del wrapper.
SwiftUI está lleno de property wrappers: @State, @Binding, @ObservedObject, @Environment. Ahora sabes cómo funcionan bajo el capó.
Métodos de instancia
Los métodos son funciones que pertenecen a un tipo:
class Counter { var count = 0
func increment() { count += 1 }
func increment(by amount: Int) { count += amount }
func reset() { count = 0 }}
let counter = Counter()counter.increment()counter.increment(by: 5)print(counter.count) // 6counter.reset()Cada método de instancia tiene un parámetro implícito self que referencia a la instancia. Normalmente se omite a menos que haya ambigüedad:
struct Point { var x = 0.0, y = 0.0
func isToTheRightOf(x: Double) -> Bool { return self.x > x // self.x es la propiedad, x es el parámetro }}mutating en structs y enums
Como vimos en el artículo #8, los value types (structs y enums) necesitan mutating para modificar self:
struct Point { var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) { x += deltaX y += deltaY }}
// Incluso puedes reemplazar self completamente:enum TriStateSwitch { case off, low, high
mutating func next() { switch self { case .off: self = .low case .low: self = .high case .high: self = .off } }}
Type methods: static y class
Los type methods se llaman sobre el tipo, no sobre una instancia:
struct LevelTracker { static var highestUnlockedLevel = 1 var currentLevel = 1
static func unlock(_ level: Int) { if level > highestUnlockedLevel { highestUnlockedLevel = level } }
static func isUnlocked(_ level: Int) -> Bool { return level <= highestUnlockedLevel }
// @discardableResult: calling this method and ignoring its Bool return value won't trigger a compiler warning @discardableResult mutating func advance(to level: Int) -> Bool { if LevelTracker.isUnlocked(level) { currentLevel = level return true } return false }}
LevelTracker.unlock(2)var tracker = LevelTracker()tracker.advance(to: 2) // trueEn classes, usa class func en lugar de static func cuando necesites que subclasses puedan hacerle override — más sobre eso en el #10.
Subscripts: acceso con corchetes
En el ejemplo de abajo, assert(...) hace crashear el programa durante el desarrollo si su condición es falsa — aquí protege contra índices fuera de rango:
struct Matrix { let rows: Int, columns: Int var grid: [Double]
init(rows: Int, columns: Int) { self.rows = rows self.columns = columns grid = Array(repeating: 0.0, count: rows * columns) }
subscript(row: Int, column: Int) -> Double { get { assert(row >= 0 && row < rows && column >= 0 && column < columns) return grid[(row * columns) + column] } set { assert(row >= 0 && row < rows && column >= 0 && column < columns) grid[(row * columns) + column] = newValue } }}
var matrix = Matrix(rows: 2, columns: 2)matrix[0, 1] = 1.5matrix[1, 0] = 3.2print(matrix[0, 1]) // 1.5Los subscripts pueden tomar cualquier número de parámetros, ser read-only o read-write, y pueden ser type subscripts (static subscript).
La memoria: cómo las propiedades definen el layout
Las stored properties de un tipo definen su memory layout:
struct UserProfile { let id: Int // 8 bytes var name: String // 16 bytes (the String struct footprint; its backing character buffer is heap-allocated for larger strings) var age: Int // 8 bytes var isActive: Bool // 1 byte} // + 7 bytes de padding para llegar al stride
MemoryLayout<UserProfile>.size // 33 bytes (8 + 16 + 8 + 1)MemoryLayout<UserProfile>.stride // 40 bytes (con alignment padding)MemoryLayout<UserProfile>.alignment // 8 bytes
Las computed properties no aparecen en el layout. No importa cuántas computed properties tengas — el tamaño del tipo no cambia:
struct Circle { var radius: Double // 8 bytes — la ÚNICA propiedad que ocupa espacio
var diameter: Double { radius * 2 } // 0 bytes var circumference: Double { .pi * diameter } // 0 bytes var area: Double { .pi * radius * radius } // 0 bytes}
MemoryLayout<Circle>.size // 8 bytes — solo radiusDiseña tus tipos pensando en qué necesita ser stored (dato real que cambia y se necesita persistir) y qué puede ser computed (derivado de otros datos). Menos stored properties = menos memoria por instancia.
Recapitulación
- Stored properties — dato real, ocupan bytes en el memory layout
- Computed properties — funciones disfrazadas, zero storage, pueden ser inline
- Lazy properties — se alocan en demanda, útiles para objetos costosos
- Property observers — willSet/didSet, no agregan storage, SE-0268 optimiza oldValue
- Type properties —
static, una copia compartida, inicializada exactamente una vez incluso entre hilos (víaswift_once) - Property wrappers — lógica reutilizable con
@, base de SwiftUI - Instance methods —
selfimplícito,mutatingpara value types - Type methods —
static func/class func, se llaman sobre el tipo - Subscripts — acceso con
[], como computed properties para colecciones - Memory layout — solo stored properties definen el tamaño; computed = free
Lo que viene
En el próximo artículo exploramos Herencia, Inicialización y Deinicialización — la trinidad que define el ciclo de vida de las classes. Veremos designated vs convenience initializers, two-phase initialization, failable init, y deinit — el último acto antes de que ARC libere la memoria.
Nos vemos la próxima semana.
Las propiedades son el ADN de tus tipos. Stored properties definen qué sabe tu tipo — y cuánta memoria consume. Computed properties definen qué puede derivar — sin costo. Diseña tus tipos con esa distinción en mente.
Referencias
Relacionados
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #8: Structs vs Classes — la decisión que define tu app
Value semantics vs reference semantics, static vs dynamic dispatch, y por qué Apple recomienda structs por defecto. El artículo que cambia cómo piensas en Swift.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #7: Enumeraciones — más que una lista de casos
Raw values, associated values, enums recursivos con indirect, y cómo el compilador elige la representación mínima en memoria.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #6: Closures — capturas, memoria y poder funcional
Closure expressions, captura de valores, capture lists, @escaping vs non-escaping y por qué los closures son reference types que viven en el heap.