Bug #16457
closed
Invisible keys on hash when defining hash with Hash.new({})
Added by Farhad (Farhad Eyvazli) over 4 years ago.
Updated over 4 years ago.
Description
When using Hash.new()
to initialize a hash, we all know for undefined it will return specific value which sent as a parameter to the ```Hash.new``
But when doing something like that keys get invisible
my_hash: Hash.new({})
my_hasy[:my_key] #=> {}
my_hash[:my_key].merge!(value: '')
my_hash.keys #=> []
my_hash.fetch(:my_key) #=> KeyError: key not found: :my_key
my_hash.dig(:my_key) #=> {:value=>""}
my_hash[:my_key] #=> {:value=>""}
Maybe it's normal behavior because, for each missing key, it initialize new empty has and merge it to that. But I'm not sure it can cause a memory leak or not when removing the main hash (:my_hash)
The logic of this behavior is:
-
my_hash[:my_key]
returns my_hash.default
(not assigning it to any key)
-
my_hash[:my_key].merge!(value: '')
updates this object
- Now this object is available for ANY key in its updated form, not only
:my_key
:
my_hash = Hash.new({})
my_hash.default # => {}
my_hash[:my_key] #=> {}
my_hash[:my_key].merge!(value: '')
my_hash.default # => {:value=>""}
# Nothing special about my_key here, you just have your default:
my_hash.dig(:my_key) #=> {:value=>""}
my_hash[:my_key] #=> {:value=>""}
# It would be the same with any key:
my_hash[:any_other_key] #=> {:value=>""}
...even if a bit surprising on a first occurence, nothing is "hidden" here ;)
This types of subtle errors can be avoided by:
# 1. Either passing blocks instead of single value:
my_hash = Hash.new { {} }
my_hash[:my_key].merge!(value: '')
my_hash[:my_key] #=> {}
# 2. Passing unchangeable values as defaults:
my_hash = Hash.new({}.freeze)
my_hash[:my_key].merge!(value: '') # FrozenError (can't modify frozen Hash: {})
So does it mean when main hash lost his references, this new allocated hash will also be collected by GC? I haven't get a chance to test it (also don't know how to do proper test :) )
- Status changed from Open to Rejected
This is as per spec.
You typically never want a hash's default to be mutable. Try to name that default in your head (e.g. X={}; my_hash= Hash.new(X)
, this might help understand it.
You may want to do instead: Hash.new{ |h, k| h[k] = {} }
which creates a new hash for every key.
Further questions are probably better asked on StackOverflow or similar.
my_hash = Hash.new({})
my_hash[:my_key].merge!(value: '')
id = my_hash.default.object_id
p ObjectSpace.each_object.find { |o| o.object_id == id }
#=> {:value=>""} -- it exists
my_hash = nil # allow myhash __and its default__ to be garbage-collected
GC.start # collect garbage
p ObjectSpace.each_object.find { |o| o.object_id == id } # try to find it
#=> nil -- no such object, yes, it got collected
Also available in: Atom
PDF
Like0
Like0Like0Like0Like0