TLDR
if you define a equality method for a class you must also implement
def hash
.
Ruby has a lot of methods that have to do something with equality, like ==
, ===
, eql?
, equal?
. This card should help you differentiate between those and give you hints on how to implement your own equality methods in a safe manner.
Differences between the methods
for everyday use: ==
When you compare two objects in ruby, you most often see the use of foo == bar
. By default the ==
operator inherits from Object
and is implemented as object identity (see equal?
below).
If you want to give this method more meaning for your class you can override this. For a PORO (Plain Old Ruby Object) that holds some attributes it is pretty common to compare the equality of all attributes instead:
def ==(other)
other.instance_of?(self.class) &&
other.street == street &&
other.zip == zip
end
for hash equality: eql?
The eql?
method is closely connected to the hash
method. In ruby any object can be used as a key in hashes (using the hash rocket notation a_hash = { obj => 'some value' }
).
To find objects by keys (e.g. a_hash[obj]
), the keys are not compared using one of the discussed equality methods, but by comparing the values of the hash method.
hash
must return an integer that is equal for objects that are equal according to the eql?
method. This makes comparison cheaper (and allows hashes to be organized efficiently in a hash table).
Thus we have to remember to always override hash
when eql?
is overridden. Also methods like Array#uniq
rely on this to be implemented correctly.
Overriding eql?
is not enough to get hash equality right:
class Person
attr_accessor :name
def initialize(name)
self.name = name
end
def eql?(other) # not enough for hash equality
other.name == name
end
end
bob = Person.new('bob')
phone_list = {
bob => '0821 123',
}
phone_list[bob] # returns '0821 123'
phone_list[Person.new('bob')] # returns nil
[bob, Person.new('bob')].uniq
[
[0] #<Person:0x0000559054263420 @name="bob">,
[1] #<Person:0x000055905404ee50 @name="bob">
]
Only after implementing hash
we get the expected results:
class Person
...
def hash
name.hash
end
end
bob = Person.new('bob')
phone_list = {
bob => '0821 123',
}
phone_list[bob] # returns '0821 123'
phone_list[Person.new('bob')] # now also returns '0821 123'
[bob, Person.new('bob')].uniq
[
[0] #<Person:0x000055bdd7167ae8 @name="bob">
]
In most cases it makes sense to alias eql?
to ==
. This, however, is not always the case. Numeric
types, for example, perform type conversion across ==
, but not across eql?
:
1 == 1.0 # true
1.eql? 1.0 # false
for case statements: ===
The triple equals operator ===
is used for case-equality. In general it tells if the object on the right "belongs to" or "is a member of" the object on the left:
String === "hello" # true
Range === (1..10) # true
Integer === 3.0 # false
("a".."f") === "d" # true
It is mainly used (implicitly) in case statements:
case host
when Host
# ...
when /www\..*/
# ...
end
Your custom classes may safely override ===
to provide meaningful semantics in case
statements.
for object identity: equal?
The equal?
method is not one that you use very frequently as it checks for object identity. For
x.equal?(y)
to be true
, both x
and y
need to reference the identically same object. You should never need to override equal?
.
Recommendations for overriding equality methods
- if you never compare objects for equality, use them as hash keys or in arrays, don't override equality methods at all
- if you implement a wrapper class, consider using equality methods of the wrapped classes instead
- in most cases
eql?
and==
should be aliased - when you use your class as hash keys or arrays override
eql?
- whenever
eql?
is overridden, overridehash
accordingly
Here is an example implementation for a basic class that holds two attributes:
class Event
attr_accessor :title, :date
def eql?(other)
# check that class is right
return false unless other.instance_of?(self.class)
# check that all attributes are equal
title == other.title && date == other.date
end
# make == and eql? to be the same
alias_method :==, :eql?
def hash
# calculate hash based on all attribute hashes
title.hash ^ date.hash
end
end
An even more generic implementation might look like this:
def hash
hash_attributes.hash
end
def eql?(other)
return false unless other.instance_of?(self.class)
other.send(:hash_attributes) == hash_attributes
end
alias_method :==, :eql?
private
def hash_attributes
[title, date]
end