Edited at

Ruby におけるハッシュ (Hash) と構造体 (Struct) の使い分け

More than 3 years have passed since last update.


概要

Ruby ではハッシュがよく使われるが、実は構造体も使える。

# ハッシュ

person = {name: "Taro", age: 16}
p person[:name]

# 構造体
Person = Struct.new(:name, :age)
person = Person.new("Taro", 16)
p person.name

どう使い分けるとよいのだろうか?


参考


ポイント1: ハッシュは動的、構造体は静的

ハッシュは存在しない要素を参照したり、要素をあとから追加できます。一方、構造体は初めに定義した要素しか扱えません。

# ハッシュ

person = {name: "Taro", age: 16}
person[:tel] # => nil
person[:sex] = "man" # 要素が追加される

# 構造体
Person = Struct.new(:name, :age)
person = Person.new("Taro", 16)
person.tel # => NameError: undefined method 'tel'
person.sex = "man" # => NameError: undefined method 'sex='

この特性ゆえ、実行時まで属性名が決まらないような場合は、ハッシュの方がきれいに対応できます。

key = ARGV[0]

# ハッシュ
p some_hash[key] # => なんとかなる

# 構造体
p some_struct.key # => ムリ
p some_struct.send(key) # => なんとかなるけど気持ち悪い


ポイント2: 等価の基準が違う

構造体は、たとえ内容が全く同じでも、元になった構造体自体が違う場合、等価になりません。

Person = Struct.new(:name, :age)

person = Person.new("Taro", 16)

Robot = Struct.new(:name, :age)
robot = Robot.new("Taro", 16)

p person == robot # => false. 越えられないカベ


ポイント3: to_a の挙動が違う

ハッシュの方は key-value のペアになった配列が得られます。

# ハッシュ

person.to_a
=> [[:name, "Taro"], [:age, 16]]

# 構造体
person.to_a
=> ["taro", 16]


ポイント4: 構造体はインスタンス生成に属性名がいらない

属性名が長くなりがちなときや、インスタンスをいくつも作るときにスッキリ。

# ハッシュ

person1 = {name: "Taro", age: 16, most_favorite_movie_ever_seen: "Toy Story"}
person2 = {name: "Jiro", age: 17, most_favorite_movie_ever_seen: "Toy Story 2"}
person3 = {name: "Maro", age: 18, most_favorite_movie_ever_seen: "Toy Story 3"}

# 構造体
Person = Struct.new(:name, :age, :most_favorite_movie_ever_seen)
person1 = Person.new("Taro", 16, "Toy Story")
person2 = Person.new("Jiro", 17, "Toy Story 2")
person3 = Person.new("Maro", 18, "Toy Story 3")


結論と感想

ポイント1の違いが一番大きいでしょう。参考リンク先では、例えば「文章中に出てくる単語をキー、その出現回数をバリューにしたいとき」のような動的な処理のときはハッシュがよいだろう、と結論付けられています。私もそう思います。

個人的に感じている構造体の便利ポイントは、単純に「ドット演算子が使える」ことです。これによって、でかいオブジェクトを返してくるメソッド(例えば open など)のスタブ化が簡単にできます。


hoge.rb

require "open-uri"

class Hoge
def call_api
response = open "http://something-api/"
response.read if response.status[0] == "200"
end
end



hoge_spec.rb

it '接続成功時は "Success" を返す' do

hoge = Hoge.new

# read と status に応答できるオブジェクトを即席で作る
dummy = Struct.new(:read, :status).new('{"result": "Success"}', ["200", "OK"])
# dummy を返すように open メソッドをスタブ化
allow(hoge).to receive(:open).and_return(dummy)

expect(hoge.call_api).to eq '{"result": "Success"}'
end