Edited at

Ruby: Hash.new とデフォルト値

More than 3 years have passed since last update.

Hash.new を呼ぶ時に、キーが存在しない場合の値(デフォルト値)を設定できます。

本稿はそのメモです。


Hash.new

Hash.new については上のリファレンスマニュアルを参照してください。

デフォルト値の設定について要約すると、以下のようになります。


  • 引数で与えると、そのオブジェクトをデフォルト値として返す

  • ブロックを与えると、それを評価した値をデフォルト値として返す

  • ブロックの引数には、ハッシュ自身とキーが渡される

また、デフォルト値がハッシュにセットされるかどうかについては、以下のようになります。


  • デフォルト値が自動的にハッシュにセットされることはない

  • ブロックで与えた場合は、ブロック内でセットすることが可能

(以降、デフォルト値として配列(Arrayオブジェクト)を返す場合を例にします)

Hash.new []                 # (1) 引数でデフォルト値を与える。(デフォルト値はハッシュにセットされない)

Hash.new {[]} # (2) ブロックでデフォルト値を与える
Hash.new {|h,k| h[k] = []} # (3) (2)と同じ、さらに、デフォルト値をハッシュにセットする

※ コメント欄の @scivola のコメントも参照ください。


実際の動き

h1 = Hash.new []                         # 引数のオブジェクト [] を x とすると、

p a1 = h1[:key1] #=> [] # a1 = x と同じ意味
p a2 = h1[:key2] #=> [] # a2 = x と同じ意味

p a1.equal? a2 #=> true # a1 も a2 も x
p a1.equal? h1[:key1] #=> true # a1 も h1[:key1] も x

a1 << 'hello'
# a1, a2, h1[:key1], h1[:key2] とも
p a1 #=> ["hello"] # 全て同じオブジェクト x を参照している
p a2 #=> ["hello"]
p h1[:key1] #=> ["hello"]
p h1[:key2] #=> ["hello"]

p h1.size #=> 0 # デフォルト値はセットされていない

h2 = Hash.new {[]}

p a1 = h2[:key1] #=> [] # a1 = [] と同じ意味 (オブジェクトが新しく作られる)
p a2 = h2[:key2] #=> [] # a2 = [] と同じ意味 (オブジェクトが新しく作られる)

p a1.equal? a2 #=> false # a1 と a2 は異なるオブジェクト
p a1.equal? h2[:key1] #=> false # h2[:key1] はこの時点で新しく作られたArrayオブジェクト

a1 << 'hello'
# a1, a2, h1[:key1], h1[:key2] とも
p a1 #=> ["hello"] # 全て異なるオブジェクトを参照している(することになる)
p a2 #=> []
p h2[:key1] #=> [] # h2[:key1] はこの時点でさらに新しく作られたオブジェクト
p h2[:key2] #=> [] # h2[:key2] もこの時点でさらに新しく作られたオブジェクト

p h2.size #=> 0 # デフォルト値はセットされていない

h3 = Hash.new {|h,k| h[k] = []}

p a1 = h3[:key1] #=> [] # a1 = h3[:key1] = [] と同じ意味 (このオブジェクトを x とする)
p a2 = h3[:key2] #=> [] # a2 = h3[:key2] = [] と同じ意味 (このオブジェクトを y とする)

p a1.equal? a2 #=> false # x.equal? y と同じ意味
p a1.equal? h3[:key1] #=> true # x.equal? x と同じ意味

a1 << 'hello'

p a1 #=> ["hello"] # a1 と h3[:key1] は同じオブジェクト x を参照している
p a2 #=> [] # a2 と h3[:key2] は同じオブジェクト y 参照している
p h3[:key1] #=> ["hello"] # h3[:key1] と h3[:key2] は異なるオブジェクトを参照している
p h3[:key2] #=> []

p h3.size #=> 2 # デフォルト値はセットされている


既存のオブジェクトを包んだクロージャをブロックとして渡せば、引数でデフォルト値を渡した場合と同様に、毎回同じオブジェクトを返すようにすることもできます。

a = []                          # オブジェクトはあらかじめ作っておく

h2 = Hash.new {a} # クロージャを渡す

a = []                          # オブジェクトはあらかじめ作っておく

h3 = Hash.new {|h,k| h[k] = a} # クロージャを渡す

実際の動きです。

a = []                                   # オブジェクトはあらかじめ作っておく

h2 = Hash.new {a} # クロージャを渡す

p a1 = h2[:key1] #=> [] # a1 = a と同じ意味
p a2 = h2[:key2] #=> [] # a2 = a と同じ意味

p a1.equal? a2 #=> true # a1 も a2 も a
p a1.equal? h2[:key1] #=> true # a1 も h1[:key1] も a

a1 << 'hello'
# a1, a2, h1[:key1], h1[:key2] とも
p a1 #=> ["hello"] # 全て同じオブジェクト a を参照している
p a2 #=> ["hello"]
p h2[:key1] #=> ["hello"]
p h2[:key2] #=> ["hello"]

p a #=> ["hello"]

a = []                                   # オブジェクトはあらかじめ作っておく

h3 = Hash.new {|h,k| h[k] = a} # クロージャを渡す

p a1 = h3[:key1] #=> [] # a1 = a と同じ意味
p a2 = h3[:key2] #=> [] # a2 = a と同じ意味

p a1.equal? a2 #=> true # a1 も a2 も a
p a1.equal? h3[:key1] #=> true # a1 も h1[:key1] も a

a1 << 'hello'
# a1, a2, h1[:key1], h1[:key2] とも
p a1 #=> ["hello"] # 全て同じオブジェクト a を参照している
p a2 #=> ["hello"]
p h3[:key1] #=> ["hello"]
p h3[:key2] #=> ["hello"]

p a #=> ["hello"]


クロージャを与えた場合、クロージャが外の環境へ参照している変数を書き換えると、デフォルト値を動的に変えられます。

a = []                         # このオブジェクトを x とする

h3 = Hash.new {|h,k| h[k] = a}

h3[:key1] << 'hello'
h3[:key1] << 'world'

p h3[:key1] #=> ["hello","world"] # オブジェクト x を参照
p h3[:key2] #=> ["hello","world"] # 〃

a = [] # このオブジェクトを y とする

p h3[:key1] #=> ["hello","world"] # オブジェクト x を参照
p h3[:key2] #=> ["hello","world"] # 〃
p h3[:key3] #=> [] # オブジェクト y を参照

h3[:key3] << 'goodby'

p h3[:key1] #=> ["hello","world"] # オブジェクト x を参照
p h3[:key2] #=> ["hello","world"] # 〃
p h3[:key3] #=> ["goodby"] # オブジェクト y を参照
p h3[:key4] #=> ["goodby"] # 〃

p a #=> ["goodby"] # オブジェクト y を参照


比較

# 用例                                返るオブジェクトは?          ハッシュにセットは?

Hash.new [] 同じもの(new の引数) されない

Hash.new {[]} 毎回作られる されない

Hash.new {|h,k| h[k] = []} 毎回作られる される

msg = []
Hash.new {msg} 同じもの(既存オブジェクト) されない

msg = []
Hash.new {|h,k| h[k] = msg} 同じもの(既存オブジェクト) される

上の「(オブジェクトが)毎回作られる」について注意です。

上のパターンは、オブジェクトが Array でなくとも、Hash、String、その他の一般的なクラスのインスタンスでも通用します。

ですが、Fixnum、Float、Symbol、TrueClass, FalseClass, NilClass のように、リテラル表現が同じものは同じオブジェクトでありインスタンスが new されないもの(これらのインスタンスは immutable(不変)です)については以下のようになります。

# 用例                                返るオブジェクトは?          ハッシュにセットは?

Hash.new 0 同じもの(new の引数) されない

Hash.new {0} 同じもの(同じリテラル) されない

Hash.new {|h,k| h[k] = 0} 同じもの(同じリテラル) される

num = 0
Hash.new {num} 同じもの(既存オブジェクト) されない

num = 0
Hash.new {|h,k| h[k] = num} 同じもの(既存オブジェクト) される

リテラル表現についての参考。(equal? はオブジェクトが「同一」かどうか判定するメソッドです)

p 1.equal? 1                         #=> true    (Fixnum)

p 1.0.equal? 1.0 #=> true (Float)
p :s.equal? :s #=> true (Symbol)
p true.equal? true #=> true (TrueClass)
p false.equal? false #=> true (FalseClass)
p nil.equal? nil #=> true (NilClass)

p 1_0000_0000_0000_0000_0000.equal? 1_0000_0000_0000_0000_0000 #=> false (Bignum)
p 's'.equal? 's' #=> false (String)
p [].equal? [] #=> false (Array)
p {}.equal?({}) #=> false (Hash)
p //.equal? // #=> false (Regexp)
p (0..1).equal? (0..1) #=> false (Range)


まとめ

デフォルト値の与え方はそれぞれのパターンで動きが異なるので、プログラムの仕様/設計/実装方針などに合わせて使い分けましょう。

でも、個人的には以下のパターンでだいたいの場面で間に合うと思います。

Hash.new {|h,k| ...したければ何かする... ; h[k] = デフォルト値 }


(追記:2014/12/26)

上の場合、既に説明した通り、ブロックを評価した値がデフォルト値として返ります。

ここで言うデフォルト値は、ハッシュに対して存在しないキーの値を取得した時の返り値です。

h = Hash.new {|h,k| h[k] = []}

p x = h[:key] #=> [] # ここで言うデフォルト値は x に代入される値。[] のこと。

これも説明したことですが、ブロックの中で「h[k] = []」をしているので、ブロック評価の副作用として、ハッシュに値がセットされます。

ハッシュにセットされる値は、「h[k] = []」の右辺の [] です。

上の場合は、ブロックの評価値 (評価については @scivola さんのコメントを参照ください) がハッシュにセットした値([])になります。これがデフォルト値になるのは既に説明した通りです。

つまり、ハッシュにセットした値とデフォルト値が一致しています。(というか、させています。)

しかし、以下のような場合はハッシュにセットする値とデフォルト値が一致しないので注意してください。

h = Hash.new {|h,k| h[k] = [] ; 0 }  # ハッシュにセットする値は []、デフォルト値は 0

以下のようになります。

h = Hash.new {|h,k| h[k] = [] ; 0 }

p h[:key] #=> 0 # デフォルト値として 0 が返っているが、ハッシュには [] がセットされている

p h[:key] #=> [] # ハッシュにセットされている値が返る

まとめますが、Hash.new にブロックを与えた場合に念頭に置くことは以下です。


  • ブロックを与えると、それを評価した値をデフォルト値として返す

ハッシュとキー(h と k)をブロックの引数にもらえるので


  • ブロックで与えた場合は、ブロック内で(ハッシュに値を)セットすることが可能

ということです。

ブロックで何をするかは自由です。ハッシュにセットする/しない、セットする値はデフォルト値/他の値、あるいは異なるキーに値をセットする/しない等も、プログラマ次第になります。


おわりに

本稿内容の動作確認は以下の環境で行っています。


  • Ruby 2.1.5 p273