Swift: Closures Avançadas do Swift
As closures desempenham um papel central na programação assíncrona, mas também introduzem desafios em torno do gerenciamento de ciclo de vida e vazamentos de memória. Esta lição explora o uso avançado de closures para que você possa usá-las com segurança e eficiência em cenários complexos de aplicação.
1. O Que Você Vai Aprender
- Cenários de uso e ciclo de vida de closures
@escaping - Avaliação preguiçosa com
@autoclosure - Listas de captura de closure e como elas resolvem ciclos de retenção
- Evitando vazamentos de memória com
weakeunowned - Entendendo quando as closures capturam valores
2. Uma História Real de um Desenvolvedor Mobile
(1) Problema: Falhas em Callback de Rede e Vazamentos de Memória
Charlie está construindo um app social onde os usuários postam comentários, exigindo uma chamada de API seguida de uma atualização da UI:
func postComment(text: String, onComplete: () -> Void) {
// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Compiler error! Non-escaping closure cannot be used in async callback
onComplete()
}
}
O compilador reclama — a closure executa após postarComentario retornar, mas aoCompletar é não-escaping por padrão. Pior, mesmo após usar uma closure de escape, ele encontra um vazamento de memória: o view controller é destruído, mas a closure ainda mantém uma referência a ele, causando um vazamento.
(2) Solução: @escaping + Listas de Captura
// Mark as escaping closure to allow async execution
func postComment(text: String, onComplete: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
guard let self = self else { return }
self.showSuccess()
onComplete()
}
}
Três mudanças: adicionar @escaping para marcá-la como de escape, adicionar [weak self] para uma referência fraca e usar guard let para desempacotamento seguro.
(3) Benefício: Gerenciamento Seguro de Memória
| Dimensão | Antes | Depois |
|---|---|---|
| Callback assíncrono | Erro de compilador | Funciona normalmente |
| Vazamento de memória | ViewController não pode ser liberado | weak self quebra automaticamente a referência |
| Risco de falha | Objeto já desalocado no callback | guard let detecta e retorna com segurança |
| Intenção do código | Ciclo de vida da closure implícito | @escaping declara explicitamente a semântica de escape |
3. Closures de Escape (@escaping)
Por padrão, os parâmetros de closure são não-escaping — eles executam antes da função retornar. Marcar uma closure com @escaping permite que ela escape do escopo da função.
sequenceDiagram
participant Chamador
participant Func as Função
participant Closure
Chamador->>Func: Passar closure
Note over Func: Não-escaping: executa dentro da função
Chamador->>Func: Passar closure @escaping
Func->>Closure: Armazenar em variável externa
Func-->>Chamador: Função retorna
Note over Closure: Pode ser chamada após o retorno da função
Chamador->>Closure: Executar em callback assíncrono
| Característica | Não-escaping | @escaping |
|---|---|---|
| Momento da execução | Antes da função retornar | Antes ou depois do retorno da função |
| Armazenar externamente | Não permitido | Pode armazenar em variável/propriedade |
Referência implícita a self. |
Pode ser omitida | Deve ser explícita |
| Otimização do compilador | Sem sobrecarga de gerenciamento de memória | Requer gerenciamento ARC |
| Desempenho | Melhor | Leve sobrecarga adicional |
▶ Exemplo: Usando Closures de Escape
// ============================================
// Escaping vs non-escaping closures comparison
// ============================================
import Foundation
var completionHandlers: [() -> Void] = []
// Non-escaping closure — synchronous execution inside function
func syncOperation(task: () -> Void) {
print("Starting sync task")
task()
print("Sync task finished")
}
syncOperation {
print(" Running...")
}
// Escaping closure — stored in external array
func asyncOperation(task: @escaping () -> Void) {
print("Adding async task")
completionHandlers.append(task) // Would error without @escaping
}
asyncOperation {
print(" Async task executed")
}
print("Function has returned, closure not yet executed")
// Execute stored closure later
completionHandlers.first?()
Saída:
TEXT 📖 Somente leituraStarting sync task Running... Sync task finished Adding async task Function has returned, closure not yet executed Async task executed
4. Autoclosures (@autoclosure)
@autoclosure envolve automaticamente uma expressão em uma closure, permitindo avaliação preguiçosa:
graph TB
A["assert(condicao: 2 > 1)"] --> B["Avaliação normal: calculada imediatamente"]
C["assert(condicao: 2 > 1, mensagem: \"erro\")"] --> D["@autoclosure: avaliada apenas quando a condição é falsa"]
D --> E["Evita sobrecarga de concatenação de string"]
| Cenário | Parâmetro Normal | @autoclosure |
|---|---|---|
| Momento da avaliação | Avaliada imediatamente no local da chamada | Avaliada apenas quando a closure é chamada |
| Otimização de desempenho | Sempre calculada | Calculada sob demanda |
| Sintaxe | Requer literal de closure { } |
Escreva uma expressão normal |
▶ Exemplo: Avaliação Preguiçosa com Autoclosures
// ============================================
// @autoclosure for deferred log output
// ============================================
var debugEnabled = false
func log(_ message: @autoclosure () -> String) {
if debugEnabled {
print("[DEBUG] \(message())")
} else {
print("Logging disabled, message skipped (not evaluated)")
}
}
// Even expensive string concatenation is not executed when disabled
debugEnabled = false
log("expensive " + "string " + "operation " + "skipped")
debugEnabled = true
log("this " + "will " + "be " + "logged")
Saída:
TEXT 📖 Somente leituraLogging disabled, message skipped (not evaluated) [DEBUG] this will be loggedArmadilha Comum:
@autoclosurepode facilmente esconder problemas de desempenho. Use-o apenas quando a avaliação adiada for realmente necessária — não abuse em APIs públicas. O uso excessivo reduz a legibilidade do código.
5. Listas de Captura e Ciclos de Retenção
(1) Ciclos de Referência Forte
Quando uma closure captura uma variável externa, a captura é uma referência forte por padrão. Quando uma closure e um objeto se referenciam mutuamente, ocorre um ciclo de referência forte (ciclo de retenção):
graph TB
A[ViewController] -->|referência forte| B[Propriedade closure]
B -->|referência forte| A
C[Referência mútua → Vazamento de memória]
A --> C
B --> C
(2) Sintaxe de Lista de Captura
Declare uma lista de captura no início de uma closure usando [ ]:
| Declaração | Significado | Caso de Uso |
|---|---|---|
[weak self] |
Referência fraca, self se torna optional |
Mais comum, mais seguro |
[unowned self] |
Referência não possuída, self não é optional mas deve permanecer vivo |
Apenas quando certeza de que self não será nil |
[weak delegate = self.delegate] |
Expressão de captura | Capturar uma propriedade específica em vez de self |
▶ Exemplo: Ciclos de Retenção e Soluções
// ============================================
// Closure retain cycles: weak vs unowned
// ============================================
import Foundation
class NetworkManager {
var onComplete: (() -> Void)?
func fetchData() {
// Simulate async request — escaping closure
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
print("Data fetched")
self.onComplete?()
}
}
deinit {
print("NetworkManager deinit")
}
}
class ViewController {
let manager = NetworkManager()
var data: String?
func loadData() {
// Use capture list to avoid retain cycle
manager.onComplete = { [weak self] in
guard let self = self else { return }
self.data = "New data"
print("UI updated")
}
manager.fetchData()
}
deinit {
print("ViewController deinit")
}
}
// Simulate usage and destruction
var vc: ViewController? = ViewController()
vc?.loadData()
// Release the view controller
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
print("Releasing ViewController")
vc = nil // Will not leak
}
Saída:
TEXT 📖 Somente leituraReleasing ViewController NetworkManager deinit ViewController deinit Data fetchedDica: A saída mostra que tanto
ViewControllerquantoGerenciadorRedesão adequadamente desalocados.[weak self]na closure garante que não há ciclo de retenção. Quando o callbackasyncAfterexecuta,selfjá é nil, então "UI atualizada" nunca é impresso — exatamente o comportamento seguro que queremos.
▶ Exemplo: Armadilha Comum com Propriedades de Closure
// ============================================
// Retain cycles in closure properties
// ============================================
class Counter {
var value = 0
var incrementHandler: (() -> Void)?
func setup() {
// Correct: use weak self to break retain cycle
incrementHandler = { [weak self] in
self?.value += 1
}
}
deinit {
print("Counter deinit")
}
}
var counter: Counter? = Counter()
counter?.setup()
print("Before release value = \(counter?.value ?? 0)")
counter = nil // Properly deallocated
print("Released")
Saída:
TEXT 📖 Somente leituraBefore release value = 0 Counter deinit Released
6. Exemplo Completo: Carregador Assíncrono Seguro de Imagens
// ============================================
// Complete example: Async image loader
// Features: @escaping + capture lists + memory safety
// ============================================
import Foundation
// 1. Image cache
class ImageCache {
private var cache: [String: Data] = [:]
func get(_ key: String) -> Data? { return cache[key] }
func set(_ key: String, data: Data) { cache[key] = data }
deinit { print("ImageCache deinit") }
}
// 2. Image loader (using escaping closures)
class ImageLoader {
let cache = ImageCache()
func loadImage(from url: String, completion: @escaping (Data?) -> Void) {
// Check cache
if let cached = cache.get(url) {
completion(cached)
return
}
// Simulate network request — escaping closure runs async
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
guard let self = self else {
// self already deallocated, safely return
completion(nil)
return
}
// Simulate downloading data
let mockData = Data([0x01, 0x02, 0x03])
self.cache.set(url, data: mockData)
DispatchQueue.main.async {
completion(mockData)
}
}
}
deinit { print("ImageLoader deinit") }
}
// 3. Usage (safe release test)
var loader: ImageLoader? = ImageLoader()
loader?.loadImage(from: "https://example.com/photo.jpg") { data in
if let _ = data {
print("Image loaded successfully")
}
}
// Immediately release — weak self in closure guarantees no crash
loader = nil
print("Loader released, async callback won't crash")
// Keep running to wait for async completion
RunLoop.main.run(until: Date(timeIntervalSinceNow: 2))
Saída:
TEXT 📖 Somente leituraLoader released, async callback won't crash ImageLoader deinit ImageCache deinit
❓ Perguntas Frequentes
P: Quando devo obrigatoriamente usar @escaping? R: Quando uma closure é armazenada em uma variável externa (como um array ou propriedade) ou usada em um callback assíncrono (DispatchQueue, URLSession), você deve marcá-la como @escaping. O compilador impõe isso. P: Como escolher entre weak e unowned? R: Prefira
weak.weaktorna a referência optional, segura mas exigindo desempacotamento.unownedassume que o objeto nunca será desalocado — se for, seu app trava. A menos que tenha 100% de certeza de que self sobrevive à closure, não use unowned. P: @autoclosure pode causar ciclos de retenção? R: Sim, pode. Porque @autoclosure implicitamente captura variáveis externas. Por exemplo, chamarfunc foo(_ closure: @autoclosure () -> Void)comself.algumMetodo()faz com que a closure retenha self. Use com cautela. P: Por que closures não-escaping não precisam de [weak self]? R: Closures não-escaping terminam de executar antes da função retornar e não sobreviverão ao ciclo de vida de self. O compilador Swift sabe disso, permitindo referências implícitas a self. Esta é uma das vantagens de segurança das closures não-escaping. P: self deve ser escrito explicitamente dentro de closures de escape? R: Sim. Dentro de closures@escaping, todas as referências aselfdevem ser explícitas. Este é um lembrete de sintaxe imposto pelo Swift — levando você a considerar se[weak self]é necessário. É um design importante de segurança de memória.
📖 Resumo
@escapingmarca closures que podem escapar do escopo da função, usadas para callbacks assíncronos e armazenamento de closures@autoclosureenvolve automaticamente uma expressão em uma closure para avaliação preguiçosa- Listas de captura com
[weak self]previnem ciclos de referência forte entre closures e objetos weaktorna a referência optional e requer desempacotamento;unownedassume que o objeto nunca será desalocado- Closures não-escaping podem referenciar self implicitamente com segurança; closures de escape devem referenciar self explicitamente
- Use o padrão
guard let self = selfpara desempacotar referências fracas com segurança
📝 Exercícios
- Básico: Escreva uma função
imprimirComAtrasoque recebe umaStringe uma closure@escaping () -> Void, atrasando a execução em 1 segundo usandoDispatchQueue.main.asyncAfter. Chame-a passando uma string e uma closure que imprime essa string. - Intermediário: Escreva uma classe
Loggercom um métodolog(_ mensagem: @autoclosure () -> String)que só imprime a mensagem quandoestaAtivadoé true. Demonstre o efeito da avaliação preguiçosa. - Desafio: Crie uma classe
GerenciadorTarefasque armazena internamente um array[() -> Void]de closures de escape. Forneça os métodosadicionarTarefa(_:)eexecutarTodas(). Use-a em umViewController, garantindo que não haja vazamento de memória quando oViewControllerfor desalocado (deve usar uma lista de captura).