Swift e Objective-C

[Swift básico #4] guard em Swift: achate a pirâmide do caos

Swift guard trata primeiro as condições de falha e mantém o fluxo normal plano. Confira as garantias do compilador em relação a if, o escopo do binding de opcionais e quando escolher return, throw ou continue com exemplos executáveis.

6 min de leitura
Imagem de capa de [Swift básico #4] guard em Swift: achate a pirâmide do caos

Uma observação comum em revisões de código é: “Vamos trocar isto por guard”. O código funciona exatamente igual com if, então por que mudar? Parece uma questão de estilo, mas guard é uma construção criada pelo Swift para incentivar um estilo específico: saída antecipada e caminho feliz alinhado à esquerda.

Este é o episódio 4 da série Swift básico. No artigo sobre opcionais, apresentei rapidamente guard let como ferramenta de desembrulho. Agora vamos examinar o próprio guard: como ele difere de if, o que o compilador garante e quando if — e não guard — é a escolha certa.

Se quiser revisar o estado de ausência de valor, leia O que os opcionais do Swift realmente são e como desembrulhá-los primeiro.

O problema — a pirâmide do caos

Primeiro, veja um código de antes da existência de guard. Uma função de cadastro tem muita coisa para validar: se a entrada é nil, se o formato está correto e se os termos foram aceitos.

func signUp(email: String?, password: String?, agreed: Bool) {
    if let email = email {
        if isValidEmail(email) {
            if let password = password {
                if password.count >= 8 {
                    if agreed {
                        // O que realmente queríamos fazer
                        createAccount(email, password)
                    } else {
                        showError("É necessário aceitar os termos")
                    }
                } else {
                    showError("A senha é curta demais")
                }
            } else {
                showError("Digite uma senha")
            }
        } else {
            showError("O formato do e-mail é inválido")
        }
    } else {
        showError("Digite um e-mail")
    }
}

A indentação chega a cinco níveis. Esse formato é chamado de pirâmide do caos e não é apenas feio: ele também aumenta o custo de leitura. O trabalho principal da função, createAccount, fica enterrado no nível mais profundo, enquanto o tratamento de falha, else, fica visualmente distante de cada condição. Para responder “O que acontece se a senha for curta demais?”, é preciso conferir chaves visualmente enquanto se rola a tela.

A resposta de guard — falhas primeiro, trabalho principal no caminho plano

Reescrevendo a mesma função com guard, temos isto.

func signUp(email: String?, password: String?, agreed: Bool) {
    guard let email, isValidEmail(email) else {
        return showError("Verifique o e-mail")
    }
    guard let password, password.count >= 8 else {
        return showError("Verifique a senha (8 caracteres ou mais)")
    }
    guard agreed else {
        return showError("É necessário aceitar os termos")
    }

    createAccount(email, password)
}

A estrutura foi invertida. Cada condição fica ligada ao seu tratamento de falha em um único bloco, enquanto o trabalho principal, depois de passar por todos os controles, permanece plano no fim da função. O olhar do leitor só precisa descer pelo código uma vez. A estrutura lógica da função, “pré-condições e depois trabalho principal”, agora coincide com a estrutura visual.

Esse benefício costuma ser chamado de alinhamento à esquerda do caminho feliz. O fluxo normal fica no nível zero de indentação, enquanto apenas as situações excepcionais entram em blocos. Mesmo quando a função cresce, permanece a consistência: seguir a extremidade esquerda revela o cenário normal.

Depois dos controles, o trabalho principal fica em um caminho plano e sem indentação
Depois dos controles, o trabalho principal fica em um caminho plano e sem indentação

A diferença real em relação a if — duas garantias impostas pelo compilador

É hora de perguntar: “if também não pode fazer uma saída antecipada?” Pode: escreva como if email == nil { return }. Mas guard é mais que um if invertido: ele oferece duas garantias impostas pelo compilador.

Primeiro, o bloco else precisa sair do escopo. Dentro do else de guard, o escopo atual deve ser abandonado com return, throw, continue, break ou fatalError; caso contrário, a compilação falha. Com if, é possível verificar a condição e esquecer o return, mas guard captura esse erro em tempo de compilação. “Se chegou até aqui, a condição é verdadeira” deixa de ser convenção e vira garantia.

Segundo, o valor desembrulhado permanece disponível em todo o código abaixo de guard. O binding de if let só vale dentro do bloco if, enquanto o binding de guard let pode ser usado em todo o escopo restante. O valor que passou pelo controle permanece disponível até o fim da função, sem separar artificialmente desembrulho e uso.

Juntas, essas garantias fazem guard também funcionar como documentação. O conjunto de guards no início de uma função declara sua lista de pré-condições. Contratos que não cabem na assinatura podem ser lidos nas primeiras linhas do corpo.

Quando if — e não guard — é a escolha certa

Devemos trocar todo if por guard? Não. O critério é claro: use guard para uma pré-condição, “sem isto não dá para continuar”; use if para uma bifurcação, “neste caso faça isto, naquele faça aquilo”.

// ifO lugar certo para if — os dois caminhos são fluxos normais
if user.isPremium {
    showPremiumBadge()
} else {
    showUpgradeButton()
}

Não ser premium não é uma falha. Se os dois ramos continuam como cenários normais, use if. Escrever isso com guard daria o sinal errado de que não ser premium é anormal.

Por outro lado, se a falha em uma verificação encerra a função, guard é apropriado mesmo com uma única verificação. A escolha da sintaxe comunica significado. Quem lê espera que guard indique uma pré-condição e que if indique uma bifurcação; atender a essa expectativa é a essência da legibilidade.

Vale apontar um antipadrão: preencher o bloco else de guard com lógica. Quando começam ali tentativas de recuperação, mudanças de estado ou processamentos longos, quebra-se a promessa de que a falha termina rapidamente. Se else passar de três linhas, use isso como sinal para revisar o design. Um tratamento de falha tão complexo é uma bifurcação separada ou algo que deve ser lançado para o chamador.

Se é pré-condição, use guard; se os dois caminhos são normais, use if
Se é pré-condição, use guard; se os dois caminhos são normais, use if

guard em loops e código assíncrono

guard também é usado fora de funções. Dentro de loops, ele combina com continue para expressar “pular este item”.

for item in items {
    guard item.isValid else { continue }
    guard let url = item.downloadURL else { continue }
    process(url)
}

Quando há várias condições de filtragem, guard mantém o corpo do for plano. Para condições simples, for item in items where item.isValid ou compactMap pode ser mais conciso. Se uma cláusula basta, use where; quando há desembrulho e várias etapas, guard é mais conveniente.

Em código assíncrono, combinar [weak self], familiar em closures, é praticamente um idioma.

fetchData { [weak self] data in
    guard let self else { return }
    self.update(with: data)
}

A pré-condição “não faça nada se self já tiver sido desalocado” é tratada na primeira linha, e o restante do código prossegue em um caminho plano onde self está disponível. É o ponto em que a filosofia de saída antecipada de guard encontra o gerenciamento de memória.

Resultado confirmado por execução direta

No Apple Swift 6.3.3, criei uma função que recebe String?, rejeita nil no else de guard e usa a string vinculada na linha seguinte quando há um valor.

guard=rejected,accepted:devpaw

Esse resultado coincide com meu critério para recomendar guard em uma revisão. Se uma entrada inválida encerra o caminho atual e apenas valores validados continuam no corpo principal, guard é adequado. Se verdadeiro e falso são fluxos normais de negócio, mantenho if em vez de trocá-lo apenas para parecer mais plano.

Resumo

  • guard oferece saída antecipada no nível da linguagem e transforma a pirâmide do caos em uma lista de pré-condições seguida de um corpo principal plano.
  • A diferença em relação a if é a garantia do compilador: else precisa sair do escopo, e os bindings permanecem válidos em todo o escopo posterior.
  • Critério: use guard para pré-condições cuja falha impede o avanço e if para bifurcações em que os dois caminhos são normais. A própria sintaxe comunica significado.
  • Um bloco else longo indica que guard está sendo usado incorretamente. Se o tratamento da falha for complexo, resolva-o com uma bifurcação ou lançando um erro.

A última frase mencionou “lançar um erro”. Esse é o assunto do próximo artigo: throws, as três formas de do-catch, try? e try!, além de Result — o mapa completo do tratamento de erros em Swift.

Leitura recomendada

Fontes e verificação