やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

組込ライブラリ(Hash)

 連想配列、辞書。キーが文字列の配列。順序あり(キー追加順)。

成果物

情報源

Hash

ハッシュテーブル(連想配列とも呼ぶ)のクラスです。ハッシュは任意の種類のオブジェクト(キー)から任意の種類のオブジェクト(値)への関連づけを行うことができます。

ハッシュ生成は多くの場合以下のようなリテラル (リテラル/ハッシュ式) で行われます。

{a => b, ... }   # aはキー、bは値となる
{s: b , ... }    # { :s => b, ... } と同じ。キーがシンボルの場合の省略した書き方
{"a+": b , ... } # { :"a+" => b, ... } と同じ。上の表現に空白や記号を含めたい場合

キーには任意の種類のオブジェクトを用いることができますが、以下の2つのメソッドが適切に定義してある必要があります。

  • Object#hash ハッシュの格納に用いられるハッシュ値の計算
  • Object#eql? キーの同一性判定

破壊的操作によってキーとして与えたオブジェクトの内容が変化し、Object#hash の返す値が変わるとハッシュから値が取り出せなくなりますから、 Array、Hash などのインスタンスはキーに向きません。Hash#rehash を参照。

 なにそれ怖い。

ただし、 更新不可 (Object#frozen? が true) では無い文字列をキーとして与えた場合は、文字列をコピーし、コピーを更新不可に設定 (Object#freeze) してキーとして使用します。この為、キーとして使われている文字列を更新しようとすると例外 FrozenError が発生するので rehash を呼ぶ必要性は生じません。

 文字列のときはよろしくやってくれるってことか。

ハッシュにはデフォルト値を設定することができます。存在しないキーを探索したときに返す値で、未設定時は nil です。デフォルト値には値形式とブロック形式があります。実際にデフォルト値がどのように扱われるかは各メソッドの説明を参照してください。

 エラーじゃなくnilなのか。なら存在しないときと値がnilのときとで違いがないってこと。微妙だな。

ハッシュに含まれる要素の順序が保持されるようになりました。ハッシュにキーが追加された順序で列挙します。

 え、マジで? これは嬉しい。ふつうハッシュといったら順序なしで困るのが通例だったから。

メンバ抜粋

特異メソッド

[] new ruby2_keywords_hash? try_convert

インスタンスメソッド

< <= == === > >= [] []= assoc clear clone compact compact! compare_by_identity compare_by_identity? default default= default_proc default_proc= delete delete_if dig dup each each_key each_pair each_value empty? eql? equal? except fetch fetch_values filter filter! flatten has_key? has_value? hash include? index inspect invert keep_if key key? keys length member? merge merge! rassoc rehash reject reject! replace select select! shift size slice store to_a to_h to_hash to_proc to_s transform_keys transform_keys! transform_values transform_values! update value? values values_at

 Arrayと同じメソッドがたくさんある。

{}, [], new

 ハッシュを生成する。

hash = {}
hash = {key: 'value', k2: 'v2'}
self[other] -> Hash

引数otherと同一のキーと値を持つ新たなハッシュを生成して返します。

引数otherがハッシュではない場合、otherのメソッドto_hashを使って暗黙の変換を試みます。

デフォルト値はコピーしません。生成されたハッシュのデフォルト値は nil です。

 えー。ハンパな差異があるのか。

引数otherと生成したハッシュは同じオブジェクトを参照することになるので、一方でキーや値に破壊的操作を行うともう片方にも影響します。

 ならばなぜデフォルト値だけはコピーも参照もしないのか。ますますもって謎仕様。

self[other] -> Hash
h = {1 => "value"}
h.default = "none"

g = Hash[h]
p g #=> {1=>"value"}

p h[:no] #=> "none"
p g[:no] #=> nil

h[:add] = "some"
p h #=> {1=>"value", :add=>"some"}
p g #=> {1=>"value"}

h[1] << 'plus' #破壊的操作
p h #=> {1=>"valueplus", :add=>"some"}
p g #=> {1=>"valueplus"}
self[*key_and_value] -> Hash

新しいハッシュを生成します。引数は必ず偶数個指定しなければなりません。奇数番目がキー、偶数番目が値になります。

このメソッドでは生成するハッシュにデフォルト値を指定することはできません。 Hash.newを使うか、Hash#default=で後から指定してください。

 ついに指定すらできなくなった。もうデフォルト値はアテにしないほうがいいな。nilであることを前提にしたほうが良さそう。となるとキーが存在しないときと、いよいよもって見分けが付かなくなってきた。

ary = [1,"a", 2,"b", 3,["c"]]
p Hash[*ary]  # => {1=>"a", 2=>"b", 3=>["c"]}
alist = [[1,"a"], [2,"b"], [3,["c"]]]
p alist.flatten(1) # => [1, "a", 2, "b", 3, ["c"]]
p Hash[*alist.flatten(1)]  # => {1=>"a", 2=>"b", 3=>["c"]}
keys = [1, 2, 3]
vals = ["a", "b", ["c"]]
alist = keys.zip(vals)     # あるいは alist = [keys,vals].transpose
p alist # => [[1, "a"], [2, "b"], [3, ["c"]]]
p Hash[alist]  # => {1=>"a", 2=>"b", 3=>["c"]}
alist = [[1,["a"]], [2,["b"]], [3,["c"]], [[4,5], ["a", "b"]]]
hash = Hash[alist] # => {1=>["a"], 2=>["b"], 3=>["c"], [4, 5]=>["a", "b"]}
new(ifnone = nil) -> Hash

空の新しいハッシュを生成します。ifnone はキーに対応する値が存在しない時のデフォルト値です。設定したデフォルト値はHash#defaultで参照できます。

ifnoneを省略した Hash.new は {} と同じです。

デフォルト値として、毎回同一のオブジェクトifnoneを返します。それにより、一箇所のデフォルト値の変更が他の値のデフォルト値にも影響します。

h = Hash.new([])
h[0] << 0
h[1] << 1
p h.default #=> [0, 1]

 意味不明。h[0] << 0というのはキー0に値0を代入したのであって、デフォルト値へのセットではないのでは? なんでデフォルト値が配列になって[0,1]になってんの? ハッシュはどこいったん?

 疑問に思ってハッシュ自体を表示してみたら空だった。

p h #=> {}

 は? なにこれ。Hash.new([])ってのはデフォルト値をいじるためのものなのか? それとも<<との組合せでこうなるの? わけわからん。

これを避けるには、破壊的でないメソッドで再代入する必要が有ります。また、このようなミスを防ぐためにもifnoneは freeze して破壊的操作を禁止しておくのが無難です。

h = Hash.new([])

p h[1]                  #=> []
p h[1].object_id        #=> 6127150
p h[1] << "bar"         #=> ["bar"]
p h[1]                  #=> ["bar"]

p h[2]                  #=> ["bar"]
p h[2].object_id        #=> 6127150

p h                     #=> {}


h = Hash.new([].freeze)
h[0] += [0] #破壊的でないメソッドはOK
h[1] << 1
# エラー: can't modify frozen Array (FrozenError)

 なにがしたいんだ? なにができるんだ? どうしてこうなった? 何一つわからん。直感的でもなんでもない。

new {|hash, key| ... } -> Hash

空の新しいハッシュを生成します。ブロックの評価結果がデフォルト値になります。設定したデフォルト値はHash#default_procで参照できます。

 なんでデフォルト値をわざわざブロックで生成するんだ。おおげさすぎる印象。

値が設定されていないハッシュ要素を参照するとその都度ブロックを実行し、その結果を返します。ブロックにはそのハッシュとハッシュを参照したときのキーが渡されます。

# ブロックではないデフォルト値は全部同一のオブジェクトなので、
# 破壊的変更によって他のキーに対応する値も変更されます。
h = Hash.new("foo")

p h[1]                  #=> "foo"
p h[1].object_id        #=> 6127170
p h[1] << "bar"         #=> "foobar"
p h[1]                  #=> "foobar"

p h[2]                  #=> "foobar"
p h[2].object_id        #=> 6127170

p h                     #=> {}

# ブロックを与えると、対応する値がまだ無いキーが呼び出される度に
# ブロックを評価するので、全て別のオブジェクトになります。
h = Hash.new {|hash, key| hash[key] = "foo"}

p h[1]                  #=> "foo"
p h[1].object_id        #=> 6126900
p h[1] << "bar"         #=> "foobar"
p h[1]                  #=> "foobar"

p h[2]                  #=> "foo"
p h[2].object_id        #=> 6126840

p h                     #=> {1=>"foobar", 2=>"foo"}

# 値が設定されていないときに(fetchのように)例外をあげるようにもできる
h = Hash.new {|hash, key|
                raise(IndexError, "hash[#{key}] has no value")
             }
h[1]
# エラー hash[1] has no value (IndexError)

 ようするにキーが存在しないとき例外発生させたければブロック式でraiseしろってことね。面倒くせ。

[], []=

 参照と代入。

h = {:ab => "some" , :cd => "all"}
p h[:ab]             #=> "some"
p h[:ef]             #=> nil

h1 = Hash.new("default value")
p h1[:non]             #=> "default value"

h2 = Hash.new {|*arg| arg}
p h2[:non]             #=> [{}, :non]
h = {}

h[:key] = "value"
p h #=>{:key => "value"}

<, <=

 <は左が右のサブセットであれば真を返す。

h1 = {a:1, b:2}
h2 = {a:1, b:2, c:3}
h1 < h2    # => true
h2 < h1    # => false
h1 < h1    # => false

 <=は左が右のサブセットまたは同じであれば真を返す。

h1 = {a:1, b:2}
h2 = {a:1, b:2, c:3}
h1 <= h2   # => true
h2 <= h1   # => false
h1 <= h1   # => true
メソッド 概要
<, <= 左が右のサブセットであれば真を返す。
>, >= 右が左のサブセットであれば真を返す。
==,eql? 左と右が同じであれば真を返す。

keys

全キーの配列を返します。

h1 = { "a" => 100, 2 => ["some"], :c => "c" }
p h1.keys           #=> ["a", 2, :c]

 キーの型がごちゃまぜでもいいんだね。怖い。

merge,merge!/update

 ハッシュをマージする。同一キーがあれば上書き、なければ追加。

h1 = { "a" => 100, "b" => 200 }
h2 = { "b" => 246, "c" => 300 }
h3 = { "b" => 357, "d" => 400 }
h1.merge          #=> {"a"=>100, "b"=>200}
h1.merge(h2)      #=> {"a"=>100, "b"=>246, "c"=>300}
h1.merge(h2, h3)  #=> {"a"=>100, "b"=>357, "c"=>300, "d"=>400}
h1.merge(h2) {|key, oldval, newval| newval - oldval}
                  #=> {"a"=>100, "b"=>46,  "c"=>300}
h1.merge(h2, h3) {|key, oldval, newval| newval - oldval}
                  #=> {"a"=>100, "b"=>311, "c"=>300, "d"=>400}
h1                #=> {"a"=>100, "b"=>200}

 merge!updateは破壊的メソッド。自分自身の内容を書き換える版。

所感

 ほかにもtransform_keysなど便利なメソッドがある。詳細はHash参照。

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux