Swift e Objective-C

[Swift básico #3] Propriedades em Swift: stored, computed, lazy e didSet

Este é o terceiro artigo da série básica de Swift. Ele resume os cinco tipos de propriedades com foco nos critérios de escolha, não em uma lista de sintaxes. A pergunta central é uma só: esse valor é armazenado, calculado e quando é criado?

7 min de leitura
Imagem de capa de [Swift básico #3] Propriedades em Swift: stored, computed, lazy e didSet

var name = "". É a linha mais usada em Swift, mas há mais opções do que parece para esse lugar: propriedades stored, propriedades computed, lazy, willSet e didSet, além de propriedades de tipo. Mesmo depois de aprender a sintaxe, a dúvida «esse valor deve ser lazy ou computed?» costuma deixar os critérios nebulosos.

Este é o terceiro artigo da série básica de Swift. Ele resume os cinco tipos de propriedades com foco nos critérios de escolha, não em uma lista de sintaxes. A pergunta central é uma só: esse valor é armazenado, calculado e quando é criado?

Antes de escolher uma propriedade de tipo por precisar de estado compartilhado, confira também O custo de testar um singleton em Swift; assim, os limites entre static let e static var ficam mais claros.

Stored vs. computed: valores na memória e valores criados sob demanda

A primeira bifurcação de uma propriedade é: stored ou computed?

Uma propriedade stored ocupa espaço real dentro da instância. Ao escrever var name = "Kim", o espaço de name é reservado na memória da instância. O tamanho de uma struct é determinado pela soma de suas propriedades stored.

Uma propriedade computed não tem espaço próprio. Ela executa código a cada acesso para produzir um valor; na prática, é uma função.

struct Rectangle {
    var width: Double   // stored
    var height: Double  // stored

    var area: Double {  // computed — sem espaço de armazenamento
        width * height
    }
}

Poderíamos ter transformado area em uma propriedade stored. Mas, nesse caso, seria preciso atualizá-la sempre que width mudasse, e qualquer divergência viraria um bug. Surge aqui o primeiro critério: não armazene valores derivados de outros; calcule-os. Manter uma única fonte de verdade elimina bugs de sincronização pela raiz.

Também é possível escrever em uma propriedade computed adicionando set. celsius é armazenado e fahrenheit é calculado; ao atribuir fahrenheit, celsius é obtido inversamente e atualizado. Mesmo aqui, só existe uma verdade armazenada; o restante são janelas para ela.

Vamos também definir quando usar uma propriedade computed em vez de uma função. A convenção das diretrizes de design de API da Apple é esta: se o cálculo é barato (geralmente próximo de O(1)), não tem efeitos colaterais e conceitualmente é «uma propriedade do objeto», use uma propriedade. Se for caro ou tiver efeitos colaterais, use um método. É por isso que array.count é uma propriedade, enquanto array.sorted() é um método.

lazy: uma propriedade stored criada no primeiro uso

lazy é a terceira opção. É uma propriedade stored, mas é inicializada no primeiro acesso, e não no momento da criação da instância.

class ImageProcessor {
    lazy var filters: [Filter] = loadExpensiveFilters()
}

Há dois casos principais. Primeiro, valores cujo custo de inicialização é alto e que talvez nem sejam usados. Preparar recursos pesados a cada criação de instância seria desperdício. Segundo, propriedades que precisam referenciar self. O valor padrão de uma propriedade stored comum é calculado antes do fim de init, portanto não pode usar self; com lazy, a inicialização é adiada até o primeiro acesso e a referência a self é permitida. É por isso que a closure imediatamente executada (= { ... }()), vista na parte de closures, costuma aparecer junto com lazy.

Há dois alertas. lazy precisa ser var. Como o valor «muda depois» (de um estado não inicializado, parecido com nil, para o valor real), ele não pode ser let. Além disso, não é thread-safe. Se várias threads fizerem o primeiro acesso ao mesmo tempo, a inicialização poderá ser executada duas vezes. Se um valor precisa ser criado uma única vez em um ambiente multithread, será necessário outro mecanismo.

A diferença para uma propriedade computed também é clara. lazy calcula uma vez, armazena o resultado e depois o retorna. computed recalcula a cada acesso. Um valor «caro, mas imutável» pede lazy; um valor «barato, mas sempre atualizado», computed.

stored é uma caixa na prateleira; computed é uma fábrica que produz sob encomenda
stored é uma caixa na prateleira; computed é uma fábrica que produz sob encomenda

willSet e didSet: reagindo a mudanças de valor

É possível adicionar observadores de mudança a uma propriedade stored. São códigos executados imediatamente antes (willSet) e depois (didSet) de o valor mudar.

class ProgressBar {
    var progress: Double = 0 {
        didSet {
            // oldValue é fornecido automaticamente
            guard progress != oldValue else { return }
            updateUI()
        }
    }
}

O uso típico é automatizar tarefas que precisam acompanhar uma mudança de valor: atualizar a UI, fazer logging ou validar valores (revertendo-os se saírem do intervalo). Você insere efeitos colaterais sem criar um setter manual e mantém a sintaxe de atribuição.

Duas regras de comportamento costumam confundir no dia a dia. Primeiro, os observadores não são chamados ao atribuir valores dentro de init. Inicialização é «configuração», não «mudança». Por isso, uma atualização de UI colocada em didSet não se aplica ao valor inicial. Segundo, alterar uma propriedade de um tipo por valor também chama o didSet da propriedade que a contém. Mesmo mudando apenas o interior da struct, como em person.name = "Lee", o didSet de person é chamado. A semântica vista na parte de tipos por valor — «modificar é substituir por um novo valor» — também vale aqui.

É importante evitar o uso excessivo de didSet. Quando a lógica se acumula nele, fica difícil rastrear o que uma simples atribuição faz. Se mudar um valor dispara uma requisição de rede, isso viola o princípio da menor surpresa. Mantenha apenas sincronizações leves em didSet e extraia a lógica pesada para métodos explícitos.

Propriedades de tipo: valores associados ao tipo, não à instância

Ao adicionar static, a propriedade passa a pertencer ao tipo em si, e não à instância.

struct APIConfig {
    static let baseURL = URL(string: "https://api.example.com")!
    static var requestCount = 0
}

Independentemente de quantas instâncias você criar, há apenas uma propriedade de tipo. Ela combina bem com conjuntos de constantes (configurações e formatadores compartilhados), e a biblioteca padrão também a usa bastante em casos como Int.max e Double.pi.

Um fato importante: static let garante inicialização lazy thread-safe. Ele é inicializado uma única vez no primeiro acesso e é seguro contra acessos simultâneos. Essa garantia existe em static, mas não em lazy var. Ela também fundamenta static let shared do singleton sem um mecanismo separado de bloqueio. Já um static var mutável é, na prática, uma variável global: prejudica o isolamento dos testes e vira candidato a condição de corrida de dados. Já explicamos em outro artigo por que singletons são considerados um antipadrão; aqui basta guardar o critério «static var é o último recurso».

Quatro perguntas bastam para decidir
Quatro perguntas bastam para decidir

Fluxograma de decisão: resumo em quatro perguntas

Os cinco tipos podem ser condensados nestas perguntas de uma linha.

  1. É derivado de outro valor? → propriedade computed. Armazene uma única verdade.
  2. A inicialização é cara ou precisa de self? → lazy var. Cuidado com o primeiro acesso multithread.
  3. Existe uma ação que acompanha a mudança de valor? → propriedade stored + didSet. Apenas tarefas leves.
  4. Uma basta, independentemente das instâncias? → static. Se for let, ainda oferece inicialização lazy segura.
  5. Se nenhuma se aplicar → uma propriedade stored comum. Na maioria dos casos, essa é a resposta.

Property wrappers como @State e @Published do SwiftUI também são, no fim, sintaxe construída sobre este sistema de propriedades. Conhecendo os princípios de stored, computed e observadores, fica claro que um wrapper «envolve uma propriedade stored e automatiza comportamentos semelhantes ao didSet». A criação de property wrappers próprios será abordada na série intermediária.

Resultados confirmados por execução direta

No Apple Swift 6.3.3, registrei no mesmo objeto o número de inicializações de lazy, os valores anterior e novo de didSet e o resultado de uma propriedade computed. A saída mostra duas leituras da propriedade lazy e a mudança da propriedade stored de 0 para 7.

properties=lazy-builds:1,didSet:0->7,computed:14

Por causa desse pequeno resultado, se um cache realmente precisa ser criado uma única vez, não confio apenas na sintaxe lazy. Se houver possibilidade de acessos simultâneos, testo o número de inicializações e verifico se é necessária sincronização adicional. Já em didSet mantenho apenas ações leves que observam mudanças de estado, como na saída acima, e movo a E/S sujeita a falhas para métodos explícitos.

Resumo

  • O primeiro critério de uma propriedade é armazenar ou calcular. Valores derivados de outros devem ser calculados para manter uma única fonte de verdade.
  • lazy é uma propriedade stored inicializada no primeiro acesso; resolve inicializações caras e referências a self. O custo é exigir var e não ser thread-safe.
  • willSet/didSet reagem a mudanças de valor, mas não são chamados em init e dificultam o rastreamento quando contêm lógica pesada.
  • static let é uma propriedade de tipo com inicialização lazy thread-safe; static var representa estado global e deve ser o último recurso.

A próxima edição é a quarta da série básica: guard. Vamos aprofundar a saída antecipada vista rapidamente na parte de opcionais e explicar por que a comunidade Swift trava uma guerra contra a indentação.

Leia também

Fontes e verificação

  • The Swift Programming Language: PropertiesSwift.org · Documentação oficial · Consultado 26 de agosto de 2026Evidência: Regras da linguagem para propriedades stored, computed, lazy, observadores e propriedades de tipo