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


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:

SWIFT
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

SWIFT
// 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.

100%
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

SWIFT
// ============================================
// 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 leitura
Starting 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:

100%
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

SWIFT
// ============================================
// @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 leitura
Logging disabled, message skipped (not evaluated)
[DEBUG] this will be logged

Armadilha Comum: @autoclosure pode 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):

100%
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

SWIFT
// ============================================
// 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 leitura
Releasing ViewController
NetworkManager deinit
ViewController deinit
Data fetched

Dica: A saída mostra que tanto ViewController quanto GerenciadorRede são adequadamente desalocados. [weak self] na closure garante que não há ciclo de retenção. Quando o callback asyncAfter executa, self já é nil, então "UI atualizada" nunca é impresso — exatamente o comportamento seguro que queremos.

▶ Exemplo: Armadilha Comum com Propriedades de Closure

SWIFT
// ============================================
// 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 leitura
Before release value = 0
Counter deinit
Released

6. Exemplo Completo: Carregador Assíncrono Seguro de Imagens

SWIFT
// ============================================
// 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 leitura
Loader 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. weak torna a referência optional, segura mas exigindo desempacotamento. unowned assume 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, chamar func foo(_ closure: @autoclosure () -> Void) com self.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 a self devem 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


📝 Exercícios

  1. Básico: Escreva uma função imprimirComAtraso que recebe uma String e uma closure @escaping () -> Void, atrasando a execução em 1 segundo usando DispatchQueue.main.asyncAfter. Chame-a passando uma string e uma closure que imprime essa string.
  2. Intermediário: Escreva uma classe Logger com um método log(_ mensagem: @autoclosure () -> String) que só imprime a mensagem quando estaAtivado é true. Demonstre o efeito da avaliação preguiçosa.
  3. Desafio: Crie uma classe GerenciadorTarefas que armazena internamente um array [() -> Void] de closures de escape. Forneça os métodos adicionarTarefa(_:) e executarTodas(). Use-a em um ViewController, garantindo que não haja vazamento de memória quando o ViewController for desalocado (deve usar uma lista de captura).
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%