No código Swift, closures estão por toda parte: condições de ordenação, handlers de conclusão de rede, ações de botões e até o body do SwiftUI. Código que passa blocos entre chaves aparece dezenas de vezes por dia. Ainda assim, quando alguém precisa explicar exatamente o que significa um closure capturar valores, por que usar «[weak self]» ou por que adicionar @escaping, é comum ficar sem palavras.
Na verdade, essas três perguntas fazem parte do mesmo assunto. Ao entender como um closure captura os valores ao redor, você entende em sequência por que ele é um tipo por referência, por que surgem referências cíclicas e por que é necessário marcá-lo como escaping. No segundo texto da série de fundamentos de Swift, vamos percorrer essa conexão em ordem.
O que é um closure — mais importante que o nome é a capacidade de «capturar»
Vamos começar pela sintaxe essencial. Closure é a forma de tratar um bloco de código executável como valor. Ele pode ser armazenado em uma variável, passado como argumento e retornado.
let add: (Int, Int) -> Int = { a, b in a + b }
add(2, 3) // 5
Na prática, uma função declarada com func também é um closure nomeado. Em Swift, funções e closures são da mesma família; a sintaxe { } é apenas a versão criada na hora e sem nome.
Mas o nome closure não vem de «bloco de código», e sim de outra propriedade: envolver e fechar sobre as variáveis ao redor, ou seja, fazer close over.
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counter = makeCounter()
counter() // 1
counter() // 2
counter() // 3
Algo estranho está acontecendo. count é uma variável local de makeCounter e deveria desaparecer quando a função retornasse, mas continua crescendo a cada chamada do closure retornado. O closure captura a variável count, que está fora do próprio corpo, e a mantém viva depois que a função termina. Essa é a essência dos closures e a origem de todo o restante deste texto.
O significado exato de capturar — não é cópia, é referência
O modo padrão de captura dos closures de Swift é por referência. Em vez de levar uma cópia do valor, eles mantêm a conexão com a própria variável. Por isso chegamos a este resultado.
var multiplier = 2
let times = { (n: Int) in n * multiplier }
times(10) // 20
multiplier = 3
times(10) // 30 — O closure vê o valor alterado
Não foi usado o multiplier (2) do momento em que o closure foi criado, mas o multiplier (3) do momento da execução. O closure carrega a própria variável, não um snapshot dela.
Se quiser fixar o valor do momento da criação, use uma lista de captura.
let times = { [multiplier] (n: Int) in n * multiplier }
times(10) // 20
multiplier = 3
times(10) // 20 — Fixado no 2momento da criação
As variáveis escritas entre colchetes são copiadas quando o closure é criado e ficam congeladas dentro dele como constantes. Em resumo: o padrão é captura por referência (uma conexão viva); a lista de captura é captura por valor (um snapshot da criação). Antes de explicar [weak self], essa distinção precisa estar clara.
Esse armazenamento de capturas também faz do closure um tipo por referência. As variáveis capturadas precisam compartilhar o ciclo de vida do closure, por isso ficam armazenadas no heap; ao copiar o valor do closure, você apenas ganha mais uma referência compartilhando esse armazenamento. É por isso que closures se comportam como classes em um Swift centrado em struct. Se a diferença entre tipos por valor e por referência não for familiar, leia primeiro o texto sobre priorizar tipos por valor.
Referência cíclica e [weak self] — o problema clássico causado pela captura
O preço da captura por referência são as referências cíclicas. O desenho é sempre o mesmo.
class ProfileViewModel {
var onUpdate: (() -> Void)?
var name = ""
func bind() {
onUpdate = {
print("Nome: \(self.name)")
}
}
}
O view model possui fortemente o closure pela propriedade onUpdate. Porém, esse closure captura self, isto é, o view model, por referência. O ciclo de posse view model → closure → view model se fecha; no mundo do ARC, os dois ficam retendo um ao outro e nunca são liberados. Mesmo fechando a tela, o view model continua na memória: o vazamento está completo.
A solução é o segundo uso da lista de captura. [weak self] instrui o closure a capturar self como referência fraca. O closure segura self, mas não é seu proprietário, então o ciclo é quebrado. Como self pode ser liberado primeiro, dentro do closure ele se torna opcional e normalmente começamos com guard let self else { return }. O padrão de saída antecipada apresentado no texto sobre opcionais aparece novamente aqui.
onUpdate = { [weak self] in
guard let self else { return }
print("Nome: \(self.name)")
}
O ponto importante é que [weak self] não é um prefixo universal. O ciclo existe quando «self possui o closure e o closure captura self». Sem essa condição, weak não é necessário. Por exemplo, o sistema descarta após a execução o closure passado para DispatchQueue.main.asyncAfter; self apenas vive um pouco mais, e isso não é vazamento. Em vez de adicionar weak por reflexo, pergunte quem possui esse closure e por quanto tempo. Casos famosos com estrutura de posse especial, como NSTimer, serão tratados em outro texto.
@escaping — quando o closure vive mais que a função
Falta a última peça. Closures recebidos como parâmetros de função às vezes recebem a anotação @escaping.
func fetchUser(completion: @escaping (User) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, _ in
let user = parse(data)
completion(user) // Executado muito depois de a função retornar
}.resume()
}
O critério é o momento da execução. Um closure executado e descartado dentro da função antes de ela retornar é non-escaping (o padrão). Se for armazenado em uma propriedade ou passado para uma tarefa assíncrona e puder executar depois do retorno, é escaping — um closure que escapa da função.
Por que obrigar essa distinção? Porque a forma de raciocinar do compilador e do desenvolvedor muda conforme o closure escapa ou não. Como o non-escaping só vive durante a execução da função, o compilador pode otimizar o armazenamento de capturas e referências cíclicas ficam fora de questão. É por isso que não precisamos nos preocupar com self em closures passados para map ou filter. Já escaping significa que o closure foi armazenado em algum lugar e vive por mais tempo, tornando-se candidato à análise de referências cíclicas. A anotação @escaping funciona como um alerta no nível da API: «este closure vai sobreviver por mais tempo; preste atenção às capturas».
Como observação, closures escaping no estilo completion handler estão diminuindo no código novo desde a introdução de async/await, mas ainda é essencial entender o conceito para ler e fazer a ponte com APIs existentes.
Três critérios para aplicar no dia a dia
Resumindo a teoria em critérios práticos, são três.
Primeiro, ao ver um closure, pergunte pelo ciclo de vida. Ele é consumido dentro da função e termina (non-escaping) ou é armazenado em algum lugar e vive mais tempo (escaping)? Essa pergunta define quanto você precisa se preocupar com a captura.
Segundo, use weak com critério, não por reflexo. Onde o ciclo de posse realmente se fecha (handlers armazenados em propriedades, callbacks semelhantes a delegates), use [weak self]; em execuções únicas sem ciclo, não é necessário. Se houver dúvida, confirme com o Leaks do Instruments ou com logs de deinit.
Terceiro, documente a intenção com uma lista de captura. Para fixar um valor, use [value]; para não assumir posse, use [weak self]. A lista de captura é mais que um recurso de desempenho: é a documentação, no código, de como esse closure se conecta ao mundo externo.
Resumo
- A essência de um closure não é o bloco de código, mas a captura: envolver e fechar sobre variáveis ao redor para prolongar seu ciclo de vida.
- A captura padrão é uma referência, não uma cópia, então o closure vê o valor no momento da execução; a lista de captura [x] fixa o valor do momento da criação.
- Como precisa compartilhar o armazenamento das capturas, o closure é um tipo por referência.
- Quando um closure pertencente a self captura self, surge uma referência cíclica; [weak self] + guard let self é a solução padrão. Sem ciclo de posse, weak também é desnecessário.
- @escaping é o alerta «closure que vive mais que a função» e marca os pontos que exigem revisão das capturas.
No próximo texto, vamos organizar as opções escondidas em uma declaração de variável: propriedades armazenadas e computadas, lazy e observadores de propriedade.
Continue lendo
- Método de fábrica estático do Swift (Static Factory Method): por que usar static func make em vez de init
- Padrão Coordinator no iOS: como tirar o código de transição de telas do view controller
- Padrão Mediator do Swift (Mediator Pattern): guia completo para delegar a comunicação entre objetos a um mediador

![Imagem de capa de [Fundamentos de Swift #2] Captura de closures, weak self e escaping](/assets/images/posts/acbedb03-d119-4e28-b248-ff183d5af89c/1.jpg)