特定の項目について値を保存したりする時にHashはよく使われているかと思います。
しかしながら、Structをあまり使ったことのない方や知らない方も初学者中心にいらっしゃるのではないでしょうか。
Structとはどのようなものなのか、またどのようなケースに有用なのかを是非知っていただきたいと思います。
Structとは
Structとは日本語では「構造体」と訳されます。
「構造体」というとっつきにくそうな名前をしていますが、臆することはないです。
「Hashのように保存できる即席クラス」と捉えてください。
Hashはキーバリューストアと呼ばれていて、「キー」と「値」が対に保存されています。
Structもほぼは同じです。
ただし「キー」ではなく「メンバ」と呼ばれており、「メンバ」と「値」が対に保存されています。
あるメンバに値を保存したり、あるメンバの値を取得したりとHashと操作感は似ています。
どのようなケースで使ったら良い?
HashとStruct両者似たようなことができますが、どのようなケースでStructを使えば良いのか?
Structが有用なケースは「項目があらかじめ決まっている」ケースです。
※他にもあると思いますが、一番頻繁に起こりうるケースを取り上げます
具体例
例えば売上の集計メソッドを作成するとします。
集計したい項目はあらかじめ決まっており、以下の通りです。
- number_of_customer 来店人数
- customer_unit_average 客単価
- total_sales 売上の合計
初期化
# 項目が決まっているため定数で定義します
# キーまたはメンバにはシンボルを用います(%i 記法を使うことでスペース区切りでシンボルの配列になります)
KEYS = %i(number_of_customer customer_unit_average total_sales).freeze
# --- Hashの初期化 ---
summary_hash = { number_of_customer: 0, customer_unit_average: 0, total_sales: 0 }
# とか
summary_hash = Hash[*KEYS.flat_map { |key| [key, 0] }]
# など
# -------------------
# --- Structの初期化 ---
# 第一引数に即席クラス名(頭文字は必ず大文字)
# 第二引数以降は設定する項目 例では*で配列を展開しています
Struct.new("Summary", *KEYS) # Struct.new("Summary", :number_of_customer, :customer_unit_average, :total_sales) と同じ
#=> Struct::Summary
# 上記でSummaryクラスがStructの中にできるので、さらにこれをnewします
# メンバの値を0で初期化
summary_struct = Struct::Summary.new(*Array.new(KEYS.size).fill(0)) # Struct::Summary.new(0, 0, 0) と同じ
#=> #<struct Struct::Summary number_of_customer=0, customer_unit_average=0, total_sales=0>
# ---------------------
項目が決まっているため、キー(メンバ)となるシンボルの配列を定数として定義しています。
Structの方はまずStruct::Summary
というクラスを生成し、その後にそのStruct::Summary
クラスのインスタンスを生成します。
ここではまだメリットデメリットはあまり見出せません。
値の登録
number_of_customer = 359 # 色々して計算できた
#--- Hash ---
summary_hash[:number_of_customer] = number_of_customer # 値をセット
summary_hash[:number_of_xxxxxxxx] = number_of_customer # もしキーが間違っていても、例外など発生せずに普通に登録できてしまう
p summary_hash
#=> { number_of_customer: 359, customer_unit_average: 0, total_sales: 0, number_of_xxxxxxxx: 359 } # 予期しないプロパティが増えてしまう
#-------------
# --- Struct ---
summary_struct.number_of_customer = number_of_customer # 値をセット
summary_struct.number_of_xxxxxxxx = number_of_customer # 初期化時に登録されていないメンバでは例外が発生(場合によっては例外処理を用意しておいた方が良い)
#=> NoMethodError: undefined method `=number_of_xxxxxxxx' for # <Struct::Summary:0x00007ffa8aba07e8> # セッターが定義されていない
p summary_struct
#=> #<struct Struct::Summary number_of_customer=359, customer_unit_average=0, total_sales=0> # 目的のメンバのみ値をセットできた
# --------------
値を登録するときにStructは役立ちます。
Hashでは最初に登録したキー以外でも、途中で別のキーが追加される可能性があります。
意図しないキーが追加されると、この後の処理で何か不都合が生じたり、余計にそれを考慮したコードの記述を余儀なくされたりするかもしれません。
また他のエンジニアがこの箇所を改修する場合に、どのようなキーが最終的に設定されているのか調べなければいけないかもしれません。
(個人的な見解: Hashは[]が面倒ですし、キーがシンボルか文字列かを迷う余地があります)
対してStructは登録されているメンバーが最初と変わらないことが保証されているため、余計な考慮などが不要です。
値の取得
# --- Hash ---
p summary_hash[:number_of_customer]
#=> 359
p summary_hash[:number_of_hogehoge] # 登録されていないキーではnilが返る(または別途設定されたデフォルト値です)
#=> nil
# -------------
# --- Struct ---
p summary.number_of_customer
#=> 359
p summary.number_of_hogehoge # 初期化時に登録されていないメンバにアクセスすると例外が発生
#=> NoMethodError: undefined method `number_of_hogehoge' for # <Struct::Summary:0x00007ffa8aba07e8> # ゲッターが定義されていない
# --------------
値を取得するときも登録するときと似ています。
Hashでは登録されていないキーではnilもしくは別途設定されたデフォルト値が返ります。
それに対してStructは初期化時に登録されていないメンバにアクセスすると同様に例外が発生します。
まとめ
Structは皆さんが普段newしまくっている(?)Classクラスのように扱えて、コードの見通しもよくなります!
項目が動的でないケースではStructを積極的に使ってみましょう!