概要
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