Project

General

Profile

Bug #16457

Invisible keys on hash when defining hash with Hash.new({})

Added by Farhad (Farhad Eyvazli) 5 months ago. Updated 5 months ago.

Status:
Rejected
Priority:
Normal
Assignee:
-
Target version:
-
ruby -v:
2.6.3
[ruby-core:96512]

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)

Updated by zverok (Victor Shepelev) 5 months ago

The logic of this behavior is:

  1. my_hash[:my_key] returns my_hash.default (not assigning it to any key)
  2. my_hash[:my_key].merge!(value: '') updates this object
  3. 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: {})

Updated by Farhad (Farhad Eyvazli) 5 months ago

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 :) )

Updated by marcandre (Marc-Andre Lafortune) 5 months ago

  • 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.

#4

Updated by zverok (Victor Shepelev) 5 months ago

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