Rubyのややこしい配列とハッシュとシンボルについて整理してみた

皆様、あけましておめでとうございます。

さて、2015年の初投稿は、私的に今最も熱いRubyネタで始めようかと思います。

まず基礎のおさらいとして、Rubyにおける配列には一次元配列のarrayクラスオブジェクトと多次元配列(連想配列)であるhashクラスオブジェクトの二種類があります。それぞれの配列オブジェクトは定義する際に要素を囲い込むリテラル記号によって以下のように区別されています。

array = ["A", "B", "C"] # 配列変数arrayを定義
hash1 = {:first => "A", :second => "B", :third => "C"} # ハッシュhash1を定義

一次元配列であるarrayクラスオブジェクトは各要素値に数値添字が自動で割り振られるため、数値添字のインデックスにて各要素にアクセスができます。

array = ["A", "B", "C"]
puts array[0] #  "A"が表示される

一方hashクラスオブジェクトはキー・バリュー型のオブジェクトなので、文字列型の添字(キー)で要素値にアクセスができます。

hash2 = {"first" => "A", "second" => "B", "third" => "C"}
puts hash2["first"] # "A"が表示される

さて、ここで一番最初の例で定義したhash1とhash2では、キーの指定の仕方が異なっていることに気づきましたでしょうか? hash1とhash2は要素値こそ一緒ですが、キーが異なるため同じハッシュではありません。

puts hash1 == hash2 # falseになります

hash1は ハッシュキーがシンボルになっているハッシュで、hash2は文字列をキーとするハッシュであるため、それぞれ異なるハッシュとなります。なお、ハッシュのキーはシンボルであろうが文字列であろうがハッシュ内でユニークである必要があります。また、文字列キーとシンボルを同名で混在させた場合、それぞれの要素が別のものとして扱われます。

hash3 = {:element => "A", :element => "B"}
hash4 = {"element" => "A", "element" => "B", :element => "A"}
puts hash3 # {:element=>"B"}と表示される
puts hash4 # {"element"=>"B", :element=>"A"}と表示される

ハッシュキーをシンボルにした場合、代入式を短縮化できます。

hash5 = {:first => "A", :second => "B"}
hash6 = {first: "A", second: "B"}
puts hash5 == hash6 # trueとなります

hash6の書き方は、JavaScriptやCSSの記述式で馴染みがあるのでそれらのコーディングをしたことがある人には使いやすいかもしれませんね。 シンボル型のハッシュの要素値を取得する場合は、下記のようにします。

puts hash5[:first] # "A"と表示される
puts hash6[:second] # "B"と表示される

私は初めてRubyのコード見た時に、シンボル型のハッシュの代入式でハッシュリテラル(接頭辞の「:」文字)が前に付いたり後ろに付いたりしているのがよく理解できず、けっこう混乱してました。それで、慣れ親しんだPHP的で一番理解し易かった文字列キーのハッシュをよく使っていました。

しかし、ハッシュはシンボルを使った方が実行速度が速いのです。

なので、シンボルが使えるならハッシュキーはシンボルにした方が良いと云われています。 それなら、ハッシュを生成する場合は、すべからくシンボルのキーを使えばいいじゃないか?──と思うかもしれません。しかし、シンボルの名前には使えない文字があります。代表的なのが「-(ハイフン)」です。

hash7 = {:first-element => "A"} # これはエラーになります
hash8 = {:"first-element" => "A"} # 「"」を含んだ文字列全体がシンボルとなります
hash9 = {:first_element => "A", :secondElement => "B", :secondelement => 2}
puts hash9 # {:first_element=>"A", :secondElement=>"B", :secondelement=>2}と表示される

利用できる文字は半角英数字に「_(アンダースコア)」の記号を加えた文字と認識しておけば十分でしょう。なお、半角アルファベットの大文字小文字はそれぞれ区別され、半角数字をシンボル名の先頭文字にすることはできません。

とはいえ、使えない文字があるからといっても、ハッシュキーには文字列キーを利用するのは極力避けた方が良いのです。 それは、シンボルは一意性オブジェクトであり、外部で破壊的メソッド等が使われた場合でもその一意性を保つことができるからです。つまり、およそ何があってもハッシュのキー情報が変化しないため、アプリケーション内外においてハッシュ構造の規則性が保たれるという利点があるのです(まぁ、形而的なメリットでしかないのですが…)。

例えば、ハッシュのキーだけを取り出して、破壊的メソッドで加工を行う場合の処理を考えてみます…。

hash10 = {"element" => "A", :element => "B"}
for i in 1..3
  hash10.keys.each do |key|
    puts key.upcase.object_id
  end
end
------
22462512 # 文字列キー"element"のオブジェクトID(forループ1回目)
217508 # シンボルキー:elementのオブジェクトID(forループ1回目)
22460736 # 文字列キー"element"のオブジェクトID(forループ2回目)
217508 # シンボルキー:elementのオブジェクトID(forループ2回目)
22460484 # 文字列キー"element"のオブジェクトID(forループ3回目)
217508 # シンボルキー:elementのオブジェクトID(forループ3回目)

これは、ハッシュのキーに対してupcaseメソッドで大文字化を行うという処理を3回繰り返すというものです。展開されたハッシュのキー(変数keyの値)はいずれも「ELEMENT」に変換されるのですが、文字列キーの場合はupcaseメソッドが呼ばれる度にオブジェクトIDが変化しています。つまりシステム内部的には異なるオブジェクトとして認識されてしまっているわけです。 一方でシンボルキーの場合は、何回upcaseメソッドが呼ばれてもオブジェクトIDに変化がありません。システム内でのシンボル「:element」は一意のままに保たれているということです。

シンボルをわかり易く言い換えるなら、「グローバル変数的文字列定数オブジェクト」というところでしょうか?(逆にわかりづらいかも…)

──とにもかくにも、Rubyのハッシュについては、まずシンボルを理解したうえで取り扱った方が良いと思った次第です。