Igualdade de objetos em Ruby

Tutoriais - 29/Ago/2019 - por André Kanamura

Vamos falar um pouco sobre comparação de objetos em Ruby

Nota: no momento de escrita deste artigo, o Ruby está na versão 2.6.3, então essa foi a documentação utilizada como referência.

Como em qualquer linguagem de programação orientada a objetos, Ruby possui formas de comparar objetos. Para verificar igualdade, por exemplo, podemos usar os métodos ==, eql?, equal? ou até ===. Sim, todos são métodos da classe Object e, por isso, podem ser sobrescritos no seu código para executar ações especializadas nas suas próprias classes. No entanto, vamos começar do início, falando um pouco sobre como funcionam esses métodos.

O === é essencialmente igual ao ==, mas é utilizado implicitamente em cenários case/when e costuma ser sobrescrito especificamente nesses casos para obter resultados mais precisos. Por isso, não vamos discorrer muito sobre ele.

Vamos demonstrar o comportamento dos demais métodos com um pouco de código:

class MinhaClasse; end

class OutraClasse; end

a = b = MinhaClasse.new
c = d = OutraClasse.new

a == b # => true
a == c # => false
c == d # => true

a.eql? b # => true
a.eql? c # => false
c.eql? d # => true

a.equal? b # => true
a.equal? c # => false
c.equal? d # => true

Quando duas variáveis fazem referência ao mesmo objeto, chamar qualquer um dos métodos retorna verdadeiro. No entanto, quando as variáveis referenciam objetos com ids diferentes, mesmo que seus parâmetros sejam idênticos, os métodos vão nos dizer que são objetos diferentes. Então, qual é a vantagem de termos tantos métodos que fazem a mesma coisa?

Vamos fazer mais dois testes com strings e com números:

str1 = 'Hello World'
str2 = 'Hello World'

str1 == str2 #=> true
str1.eql? str2 #=> true
str1.equal? str2 #=> false

value1 = 1
value2 = 1.0

value1 == value2 #=> true
value1.eql? value2 #=> false
value1.equal? value2 #=> false

Aqui já podemos notar diferenças. Comparando strings, eql? e == se comportam da mesma maneira. No entanto, quando comparamos um inteiro com um número decimal, eql? retorna falso, enquanto ==, verdadeiro. Isso acontece porque == executa conversão de tipos numéricos quando invocado, mas o eql? não. Sendo assim, 1 == 1.0 #=> true, mas 1.eql? 1.0 #=> false, afinal inteiro é diferente de decimal.

Já o equal? retorna falso nos testes com strings e com números, porque esse método verifica se as variáveis referenciam o mesmo objeto, ou seja, ele checa se possuem o mesmo id, mas não compara os valores atribuídos. Por isso, não é recomendado que o equal? seja sobrescrito. Os demais métodos, no entanto, foram desenhados de forma que possam ser sobrescritos para implementar especializações da classe Object.

Agora que entendemos as diferenças entre os métodos, vamos ver como implementar igualdade para fazer suas classes mais eficazes. No exemplo abaixo, criamos uma classe Conta que se refere a uma conta bancária e possui os parâmetros numero, nome e saldo.

class Conta
    attr_accessor :numero, :nome, :saldo
    def initialize(numero, nome, saldo)
        @numero = numero
        @nome = nome
        @saldo = saldo
    end

Se criarmos duas contas com os mesmos dados e verificarmos igualdade com ==:

conta1 = Conta.new(1234, "Paulo Cardoso", 10)
conta2 = Conta.new(1234, "Paulo Cardoso", 10)

conta1 == conta2 
#=> false

Se quisermos usar esse método para que retorne verdadeiro caso os parâmetros dos nossos objetos Conta sejam idênticos, podemos sobrescrever o método == para que ele compare os parâmetros da classe, no lugar do id dos objetos.

class Conta
  attr_accessor :numero, :nome, :saldo
  def initialize(numero, nome, saldo)
      @numero = numero
      @nome = nome
      @saldo = saldo
  end

  def == (other)
    instance_variables == other.instance_variables
  end
end

conta1 = Conta.new(1234, "Paulo Cardoso", 100)
conta2 = Conta.new(1234, "Paulo Cardoso", 100)

conta1 == conta2 
#=> true

Pronto!

Neste artigo entendemos melhor como funcionam os métodos que avaliam igualdade em Ruby e vimos como fazer classes mais eficazes, implementando o método ==.

TL;DR

  • eql? se comporta igual a ==, exceto em comparações de números, pois eql? não faz conversões de tipos numéricos.
  • === se comporta igual a ==, mas é normalmente sobrescrito para ser utilizado em case.
  • equal? verifica se as variáveis referenciam o mesmo objeto, ou seja, ele checa se possuem o mesmo id.

Referências