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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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<Genre>?
    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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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:

1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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…

1
2
3
4
5
6
7
8
9
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 uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

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