Duplicate keys with Hash#compare_by_identity
Question
What's the return value of the following Ruby code?
h = {}.compare_by_identity
h["key"] = 42
h["key"] = 84
h # => ???
The correct answer is
-
It raises
KeyError -
{ "key" => 42 } -
{ "key" => 84 } -
{ "key" => 42, "key" => 84 }Correct
Explanation
TL;DR
compare_by_identity switches the hash from comparing keys with eql? and hash to comparing them with object identity (equal?). Each "key" literal allocates a new String object with its own object_id, so the two assignments create two distinct entries: {"key" => 42, "key" => 84}.
Step by step
Every occurrence of a string literal builds a fresh object:
"key".object_id == "key".object_id # => false
In a regular hash the two keys count as the same because they are eql? and share a hash value, so the second assignment overwrites the first. After compare_by_identity, only being the exact same object counts:
h = {}.compare_by_identity
h.compare_by_identity? # => true
h["key"] = 42
h["key"] = 84
h # => {"key" => 42, "key" => 84}
Lookups follow the same rule. A third "key" literal is yet another object, identical to neither stored key, so it finds nothing:
h = {}.compare_by_identity
h["key"] = 42
h["key"] # => nil
To hit an entry you must hold a reference to the very object used as the key:
h = {}.compare_by_identity
k = "key"
h[k] = 42
h[k] # => 42
Edge cases
Symbols and small integers are immediates: every occurrence is the same object, so they still collide in an identity hash:
h = {}.compare_by_identity
h[:key] = 1
h[:key] = 2
h # => {key: 2}
The # frozen_string_literal: true magic comment deduplicates identical string literals within a file, turning the two "key" literals into one object. The original snippet then behaves like a regular hash again and ends up as {"key" => 84}.
compare_by_identity is a one-way switch on the receiver: there is no method to turn it back off.
Share this quiz
Comments
No comments yet. Be the first.