== (duplo igual), === (triplo igual) e o protocolo Equatable

TL;DR: O operador duplo-igual (==) diz se dois objetos são “iguais” ou “equivalentes”, e esse significado tem que ser implementado pelo programador (que faz isso através do protocolo Equatable).  Por outro lado, o operador triplo-igual (===) diz se duas variáveis fazem referência ao mesmo objeto — algo que o Swift faz sem precisar do programador.

No Swift, o operador de igualdade é diferente1 de muitas outras linguagens de programação.  Em geral parece funcionar muito bem, mas quando tentamos usá-lo para comparar instâncias de nossas classes ou structs, alguma coisa estranha ocorre: o Swift simplesmente se recusa a aceitar?!?

Qual é o problema?

Para entender o problema, vamos primeiro vê-lo acontecendo.  Vou começar com um exemplo simples que pode ser baixado deste link.  Você pode copiar os dois trechos de código e colá-los em um playground do Xcode.  Em seguida, crie dois objetos da classe Movie como visto a seguir, ambos exatamente com os mesmos dados:


let umFilmeQualquer = Movie(originalTitle: "Duas Horas Perdidas", year: 2018, runtimeMinutes: 120, genres: [.drama])
let outroObjetoComOsMesmosDados = Movie(originalTitle: "Duas Horas Perdidas", year: 2018, runtimeMinutes: 120, genres: [.drama])

Agora vem a pergunta pra dar aquela engasgada: O que acontece quando a gente compara os dois objetos?  Eles são iguais (porque contêm os mesmos dados) ou são diferentes (porque são dois objetos distintos)?  Em outras palavras, o que acontece no código a seguir?


if umFilmeQualquer == outroObjetoComOsMesmosDados {
print("O Swift acha que os objetos são iguais.")
} else {
print("O Swift acha que os objetos são diferentes.")
}

A resposta é: nenhum dos dois!  O Swift se recusa a executar, emitindo o erro Binary operator '==' cannot be applied to two 'Movie' operands.

Essa não! Então não posso comparar objetos?

Calma, pode sim!  Só que o Swift se recusa a responder “no chute”.  O programador é que deve dizer o que são “dois objetos iguais”, dependendo da lógica desejada.

Vamos para a prática!  Para ensinar ao Swift como comparar instâncias de uma classe, devemos implementar o protocolo Equatable.  Se você não sabe o que são protocolos ou se nunca implementou um, não se preocupe: na verdade é bastante simples!

Para prosseguir no nosso exemplo dos filmes, vamos considerar que dois filmes são “iguais” se o título original e o ano de produção for o mesmo.  Basta copiar e colar o seguinte código:


extension Movie: Equatable {
public static func == (lhs: Movie, rhs: Movie) -> Bool {
return lhs.originalTitle == rhs.originalTitle && lhs.year == rhs.year
}
}

(Eu implementei como uma extensão para não mexer no código original, mas poderia ter sido implementado diretamente na definição da classe.) Basicamente estamos ensinando ao Swift o que fazer para comparar dois objetos da classe Movie: no nosso exemplo, a correspondência ocorre quando os títulos e os anos de produção são coincidentes. Não disse que era fácil?

Nota: Não importa o nome dos dois argumentos: poderia ser a e b, ou filme1 e filme2, ou quaisquer outros nomes. A convenção é usar esses dois nomes estranhos: lhs e rhs, que significam apenas “o da esquerda” (“left-hand side”) e “o da direita” (“right-hand side”).

Comparando referências: “objetos equivalentes” vs. “o mesmo objeto”

Eu já citei em outro post que variáveis (ou constantes) que armazenam objetos na verdade guardam apenas referências a esses objetos. O Swift possui um operador especial que testa se duas variáveis estão referenciando o mesmo objeto: trata-se do triplo igual, ===, que pode ser usado exatamente como o conhecido duplo igual:


if umFilmeQualquer === outroObjetoComOsMesmosDados {
print("São duas referências para o mesmo objeto.")
} else {
print("São dois objetos diferentes, mesmo que tenham os mesmos dados.")
}
// Saída: “São dois objetos diferentes, ...”

let outraReferenciaParaUmFilmeQualquer = umFilmeQualquer

if umFilmeQualquer === outraReferenciaParaUmFilmeQualquer {
print("São duas referências para o mesmo objeto.")
} else {
print("São dois objetos diferentes, mesmo que tenham os mesmos dados.")
}
// Saída: “São duas referências para o mesmo objeto.”

Se você considera que a comparação de dois valores da sua classe deve funcionar apenas quando são referências repetidas, basta “ensinar” isso ao Swift:


extension Movie: Equatable {
public static func == (lhs: Movie, rhs: Movie) -> Bool {
return lhs === rhs // Só dá verdadeiro se for exatamente o mesmo objeto
}
}

Se você precisa implementar o protocolo Equatable antes de pensar na lógica mais detalhada para fazer comparações, essa pode ser uma implementação inicial.

Mas o Swift não poderia dar uma “implementação padrão” que compara todos os campos?

Espera: mas será que faz sentido?  Veja só: Uma das propriedades da classe Movie é isFavourite, um booleano usado para marcar os nossos filmes favoritos.  O que acontece se tiver dois registros com todos os dados idênticos: nome, ano de produção, duração, gênero; um marcado como sendo favorito e outro não.  Será que é correto considerar que são dois filmes distintos só por causa dessa diferença?  Ou seria na verdade o mesmo filme?  Outro exemplo: O filme “Wall-E”, de 2008, deve ser considerado igual ou diferente do filme “WALL-E”, de 2008?

Enfim, o Swift se recusa a fornecer uma implementação automática justamente porque o programador deve tomar decisões em cima dessas sutilezas.

Testei o triplo igual com structs, mas não funciona!

Mas não vai funcionar mesmo! Afinal de contas, o triplo igual é para ver se duas referências são para o mesmo objeto — mas os structs nunca são referências, portanto o operador simplesmente não faz sentido nesse caso. Se estiver confuso com esses conceitos, dê uma olhada neste post.

Um abraço!

  1. A piada é infame, mas vai ficar assim mesmo!

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.