Um pouco sobre os guards

TL;DR: Tudo que o guard faz poderia ser feito com if.  Mas os guards foram criados para melhorar a leitura do código, indicando um “posto de verificação” para os comandos seguintes: “Preciso que esta condição seja verdadeira, senão não é possível executar nada daqui pra frente.”

O guard é uma dessas coisas em Swift que a gente aprende como funciona, mas no começo fica sem usar porque no fundo não entende qual é a verdadeira utilidade.  Afinal, já que o guard é uma forma de teste condicional, qual é o problema em usar o velho e conhecido “if”, com o qual estamos acostumados e que nunca nos deixou na mão?

A “pirâmide da perdição”

Bom, o objetivo do guard é melhorar a legibilidade do código, para evitar aquelas situações em que tem uma sequência de “if”s um dentro do outro e, por causa da indentação acumulada, o código fica muito deslocado para a direita.

Vamos começar com um exemplo.  Digamos que eu tenha criado a seguinte classe para representar um filme (você pode baixar o código-fonte dos exemplos neste link):


public class Movie {
// [aqui tem a declaração da enum Genre]

public var originalTitle: String
public var isAdult: Bool?
public var year: Int?
public var runtimeMinutes: Int?
public var genres: Set?
public var portugueseTitle: String?
public var imdbRating: Double?
public var isFavourite: Bool

// [aqui tem o init]
}

Agora, estou querendo escolher um filme para ver. Queria uma comédia mais antiga — digamos, anterior a 1990 — e que seja bem conceituada no IMDB (quero nota mínima de 8,5). Infelizmente, como todos esses campos são opcionais, eu preciso ter o cuidado de verificar primeiro se cada campo contém nil antes de verificar se ele corresponde aos meus critérios de busca.

Assim, uma possível solução está no código a seguir:


for movie in movieCatalog {
if movie.year != nil {
let year = movie.year!
if year < 1990 { if movie.genres != nil { let genres = movie.genres! if genres.contains(.comedy) { if movie.imdbRating != nil { let imdbRating = movie.imdbRating! if imdbRating >= 8.5 {
print("Uma alternativa é: \(movie.originalTitle).")
}
}
}
}
}
}
}

Garanto que não existe nada de errado na lógica desse trecho de código. O problema é a legibilidade: com tantos blocos aninhados e níveis de alinhamento, fica fácil a gente se perder! Você já deve ter visto coisas parecidas com esse triângulo de “if”s: muitos programadores o chamam de “pyramid of doom”, ou “pirâmide da perdição”.

E tem solução?

Certo, mas como tornar esse código mais legível?

Em Swift, podemos inverter o pensamento e criar barreiras, ou “postos de verificação”, de modo a impedir a execução dos comandos seguintes se alguns critérios não forem obedecidos. No fim, o efeito final (no caso do exemplo, a impressão do título do filme) só ocorre se o filme conseguiu ser “aprovado” em todos os postos de verificação:


for movie in movieCatalog {
// ... Testes para descartar os filmes que não interessam ...

// Se chegar neste ponto, é porque o filme passou nos filtros:
print("Uma alternativa é: \(movie.originalTitle).")
}

Bem mais fácil de ler, certo? Bom, vamos ao código que está faltando:


for movie in movieCatalog {
// Primeiro filtro: Filme anterior a 1990
guard movie.year != nil else {
continue
}
let year = movie.year!
guard year < 1990 else { continue } // Segundo filtro: Filme de comédia guard movie.genres != nil else { continue } let genres = movie.genres! guard genres.contains(.comedy) else { continue } // Terceiro filtro: nota mínima no iMDB guard movie.imdbRating != nil else { continue } let imdbRating = movie.imdbRating! guard imdbRating >= 8.5 else {
continue
}

// Se chegar neste ponto, é porque o filme passou nos filtros:
print("Uma alternativa é: \(movie.originalTitle).")
}

Mais legível, não acham? O guard é uma espécie de controle de qualidade que diz: “Daqui pra frente essa condição tem que ser verdadeira, senão…” e um bloco de código que necessariamente impede a execução dos comandos seguintes.

Essa última parte é muito importante, pois é o que garante a desistência quando a condição falha. Assim, necessariamente o bloco dentro do “else” deve conter um dos seguintes comandos:

  • continue, para desistir da iteração atual e passar para a próxima rodada do laço (é o que estou usando para dizer “desista deste filme, passe pro próximo”);
  • break, para desistir do laço atual;
  • return, para desistir da função ou método atual;
  • throw, para desistir de… bom, de qualquer esperança, chutar o balde e gerar uma exceção.

Aliás, o uso de um desses comandos é obrigatório: se a gente omitir, o Swift emite o seguinte erro: 'guard' body must not fall through, consider using a 'return' or 'throw' to exit the scope.

Subindo de nível: O “guard let”

Tem uma situação bastante comum em programação, quando a gente precisa fazer a seguinte sequência:

  1. guard preliminar: Testar se um optional está guardando algum valor (ou seja, se é diferente de nil);
  2. let: Extrair o valor do optional com a exclamação (!) e guardar em uma variável local;
  3. guards adicionais: Fazer testes em cima dessa variável.

Olhe o meu código de exemplo acima: Notou que essa situação aparece três vezes, uma pra cada campo testado?

Muito bem. A próxima dica é que os dois primeiros passos — o guard do nil e o let para o valor extraído — podem ser comprimidos em um único passo!

Seja bem-vindo ao guard let. Aqui está ele em ação:


for movie in movieCatalog {
// Primeiro filtro: Filme anterior a 1990
guard let year = movie.year else {
continue
}
guard year < 1990 else { continue } // Segundo filtro: Filme de comédia guard let genres = movie.genres else { continue } guard genres.contains(.comedy) else { continue } // Terceiro filtro: nota mínima no iMDB guard let imdbRating = movie.imdbRating else { continue } guard imdbRating >= 8.5 else {
continue
}

// Se chegar neste ponto, é porque o filme passou nos filtros:
print("Uma alternativa é: \(movie.originalTitle).")
}

Entendeu como ele funciona? A ideia é: “Preciso que este valor não seja nil e preciso dele nesta nova variável, senão…” e a condição de desistência. Em um único passo!

Várias condições em um único guard

Não quero estender muito este post, mas tem mais uma dica que eu queria deixar: O guard permite listar várias condições separadas por vírgula. A desistência ocorre se qualquer condição falhar.

Isso pode ser usado para condensar um pouco mais o meu exemplo acima. Dê uma olhada na versão a seguir (e preste atenção nas vírgulas para separar as condições):


for movie in movieCatalog {
// Primeiro filtro: Filme anterior a 1990
guard let year = movie.year, year < 1990 else { continue } // Segundo filtro: Filme de comédia guard let genres = movie.genres, genres.contains(.comedy) else { continue } // Terceiro filtro: nota mínima no iMDB guard let imdbRating = movie.imdbRating, imdbRating >= 8.5 else {
continue
}

// Se chegar neste ponto, é porque o filme passou nos filtros:
print("Uma alternativa é: \(movie.originalTitle).")
}

Será que isso significa que a gente pode entrar em modo insano e colocar todas as condições em um único comando? Bom, até pode…


for movie in movieCatalog {
// Todos os filtros em cascata!
guard let year = movie.year, year < 1990, let genres = movie.genres, genres.contains(.comedy), let imdbRating = movie.imdbRating, imdbRating >= 8.5 else {
continue
}

// Se chegar neste ponto, é porque o filme passou nos filtros:
print("Uma alternativa é: \(movie.originalTitle).")
}

…mas o guard foi criado para aumentar a legibilidade, e chega um ponto em que muita coisa acontecendo em uma única linha de código acaba atrapalhando, não é mesmo? Assim, fica a dica: Use, mas não abuse!

Deixe um comentário

O seu endereço de e-mail não será publicado.

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.