きっかけ
とちぎRuby会議09にリモートで参加し、そこで見たLTに今更聞けない! Struct の使い方と今後の可能性についてでRubyのStructクラスについて初めて知った。
なにこれめっちゃ便利じゃん!となったので啓蒙も兼ねて記事を書こう、となったのがきっかけ。
Rubyはかれこれ休み休み10年間は触っているけれど未だに発見がある。素敵!学んでいないだけでは??
Structクラスとは
Ruby 2.7.0 リファレンスマニュアル Structクラス
構造体クラス。Struct.new はこのクラスのサブクラスを新たに生成します。
個々の構造体はサブクラスから Struct.new を使って生成します。個々の構造体サブクラスでは構造体のメンバに対するアクセスメソッドが定義されています。
自分の解釈で説明すると
任意の名前のプロパティ(とメソッド)を持つオブジェクトをお手軽に作成できる便利構造体クラス
使い方
# 4つのパラメーターを持ったStructのサブクラスを生成する
BreakwaterClub = Struct.new(:id, :name, :grade, :age)
# 生成したサブクラスのインスタンスを作成
bucho = BreakwaterClub.new(1, 'kuroiwa', 3)
# 作成したインスタンスはパラメーター名のアクセサメソッドを持っている
p bucho.name
#=> "kuroiwa"
bucho.age = 17
# 初期化時にセットしていなかったageがセットされている
p bucho
#=>#<struct BreakwaterClub id=1, name="kuroiwa", grade=3, age=17>
# keyword_init: true を指定することで初期化時にキーワード引数を渡せるようになる
# キーワード引数のほうがわかりやすいけど文字数は多くなるのでどちらを使うかはお好みで
BreakwaterClub = Struct.new(:id, :name, :grade, :age, keyword_init: true)
hina = BreakwaterClub.new(name: 'tsurugi', grade: 1)
Hashや配列と比べて何が嬉しいの?
Rubyでちょっとした処理を書く時によく使いがちな配列やHash。
まだ頭の中で整理できてない処理とかをアウトプットしながら整理する時とかにも使ったりする。
# 配列
bucho = [1, 'kuroiwa', 3]
bucho[0] #=>1
bucho[1] #=>'kuroiwa'
bucho[2] #=>3
# Hash
bucho = {id: 1, name: 'kuroiwa', grade: 3}
bucho[:id] #=>1
bucho[:name] #=>'kuroiwa'
bucho[:grade] #=>3
定義してないパラメーターを指定するとエラーになる
Hashで定義してたりするとTypoしているのにそれに気付かず「なぜ動かない…合っているはずなのに…」とかあるからこれはありがたい。
senpai = BreakwaterClub.new(name: 'ohno', grade: 2)
senpai[:height] #=>NameError (no member 'height' in struct)
senpai.height #=>NoMethodError
# Hashだと定義してなくても参照できてしまう
senpai[:height] #=>nil
Structクラスは配列、Hashと同じようにアクセス可能で型変換も可能
つまり上位互換って考えていいと思う。
natsumi = BreakwaterClub.new(name: 'hodaka', grade: 1)
# 配列のようにアクセスできる。indexの順番は`Struct.new`で定義した順番になる
natsumi[0] #=>nil
natsumi[1] #=>'hodaka'
# Hashのようにもアクセスできる
natsumi[:id] #=>nil
natsumi[:grade] #=>1
# 配列にもHashにも変換できる
natsumi.to_a #=>[nil, "hodaka", 1, nil]
natsumi.to_h #=>{:id=>nil, :name=>"hodaka", :grade=>1, :age=>nil}
メソッド定義が可能
Struct.new
の際ににブロックを指定することでメソッドを定義可能
BreakwaterClub = Struct.new(:id, :name, :grade, :age, keyword_init: true) do
def gakunen
"#{grade}年生"
end
end
# `Struct.new`で生成したStructクラスを継承したサブクラスを作成することでも可能
Class BreakwaterClub < Struct.new(:id, :name, :grade, :age, keyword_init: true)
def gakunen
"#{grade}年生"
end
end
Structクラス自体を継承したサブクラスを定義することは非推奨らしい。ここまだちょっとよく理解できてない。
継承元となるStructクラスが動的に生成した無名クラスなので不定なことに起因していると思う。
参考:
- 無名クラスから継承すると、何が問題なのか(Ruby)
- irbで2回以上loadすると失敗する
- 以下ドキュメント引用
ブロックを指定した場合
Struct.new にブロックを指定した場合は定義した Struct をコンテキストにブロックを評価します。また、定義した Struct はブロックパラメータにも渡されます。
Customer = Struct.new(:name, :address) do
def greeting
"Hello #{name}!"
end
end
Customer.new("Dave", "123 Main").greeting # => "Hello Dave!"
> Structをカスタマイズする場合はこの方法が推奨されます。無名クラスのサブクラスを作成する方法でカスタマイズする場合は無名クラスが使用されなくなってしまうことがあるためです。
> [SEE_ALSO] [Class.new](https://docs.ruby-lang.org/ja/latest/method/Class/s/new.html)
## まとめ
Structクラスは便利。多種多様な記述方法があるRubyらしいクラスだと思う。
### Structクラスまとめ
- 任意のパラメーター、メソッドを定義できる構造体クラス
- 配列やHashのようにアクセスでき、型変換も可能
- 初期化時に指定していないパラメーター名だとエラーになる(Hashだと`nil`になる)
### どういう時に使うと便利?
- 配列やHashですませちゃってるけどClassとして定義したほうがいいよなってとき
- ちょっとコードを書いて検証とかしたいとき
- Classを定義するほど考えがまとまってないとき
- おいそれと叩けないAPIを介したテストをやりたいとき
まだ試してませんが、RailsのRspecでのテスト時などでも使えそうだと感じました。
irbとかでも気軽に試せるので興味が出た人は是非試してみてください。