Rubyの構造体について
社内での勉強会で使用していた書籍ではじめてOpenStruct
に出会い、それに派生してRubyでの構造体について調べてまとめました。
構造体とは
そもそも構造体ってなんだろうと調べたとき、下記記事の
元々ある型の変数を寄せ集めて新しい型を作れるのが構造体の特徴です。
という説明がとてもしっくりきました。
構造体とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
Rubyで構造体を扱う
Rubyで構造体を扱うときの方法として、以下の2つについて取り上げます。
- Struct
- OpenStruct
Struct
組み込みライブラリであり、Struct.new(*args, keyword_init: nil)
という構文でStructのサブクラスを返します。
Structにはkeyword_init
という引数が存在します。これにkeyword_init: true
を指定することで、構造体の初期化時にキーワード引数を使用するようにできます。
user = Struct.new(:name, :age, keyword_init: true)
alice = user.new(name: "Alice", age: 20)
p [alice.name, alice.age] # ["Alice", 20]
これにより引数の何番目にどの値が入るんだっけ?といったことを防ぐことができます。
Ruby3.2からはこれがデフォルトになるため、わざわざkeyword_init: true
を記載する必要はなくなるようです。
OpenStruct
標準ライブラリであり、require 'ostruct'
でライブラリを読み込む必要があります。OpenStruct.new(hash = nil)
という構文でOpenStructオブジェクトを返します。
require 'ostruct'
bob = OpenStruct.new({name: "Bob", age: 25})
p [bob.name, bob.age] # ["Bob", 25]
要素を動的に追加できる点がStruct
とは異なります。
require 'ostruct'
bob = OpenStruct.new({name: "Bob", age: 25})
puts bob # #<OpenStruct name="Bob", age=25>
# 初期化後に要素を追加
bob.gender = "male"
bob.height = 170
puts bob # #<OpenStruct name="Bob", age=25, gender="male", height=170>
要素を動的に追加できることは便利でもありますが、既存のメソッドを上書きしてしまうなどの危険性もあります。
以下の記事ではmethods
メソッドが上書きされてしまうことが例として記載されています。
上記特徴やパフォーマンスが悪いことなどの理由から、OpenStructは非推奨である旨がostruct.rb
のソースコード上の注意事項に記載されています。
構造体の用途
構造体を使用するのは、「データをいくつかの固まりで保持したいが、クラスを使うまでもないとき」だと現時点で自分は考えています。
例)「名前」と「年齢」が入った「ユーザ」というデータを保持したいとき
Structを使用したとき
user = Struct.new(:name, :age, keyword_init: true)
alice = user.new(name: "Alice", age: 20)
p [alice.name, alice.age] # ["Alice", 20]
クラスを使用したとき
class User
attr_reader :name, :age
def initialize(name:, age:)
@name = name
@age = age
end
end
alice = User.new(name: "Alice", age: 20)
p [alice.name, alice.age] # ["Alice", 20]
上記のように値を保持し、値を出力するだけ、といった用途でクラスを使用するのは大げさかなと感じました。
StructとHashの使い分け
Hashを使用しても構造体と同じような要件を満たすことができます。
charlie = {name: "Charlie", age: 30}
p [charlie[:name], charlie[:age]] # ["Charlie", 30]
StructとHashには以下のような違いがあります。
- Structは要素の取得時にシンボルと文字列の区別がない
- Hashは要素を動的に追加できる
- 存在しない要素名にアクセスしたとき
- 要素の取得方法
Structは要素の取得時にシンボルと文字列の区別がない
Structは要素の取得時に使用するキーに対して、シンボルと文字列、どちらでも取得ができます。
Hashではシンボルで要素を追加した場合、文字列では取得できません。
user = Struct.new(:name, :age, keyword_init: true)
bob = user.new(name: "Bob", age: 20)
p [bob["name"], bob[:age]] # ["Bob", 20]
charlie = {name: "Charlie", age: 30}
p [charlie["name"], charlie[:age]] # [nil, 30] ← "name"というキーは存在しない
Hashは要素を動的に追加できる
user = Struct.new(:name, :age, keyword_init: true)
bob = user.new(name: "Bob", age: 20)
bob.gender = "male" # エラー undefined method `gender='
charlie = {name: "Charlie", age: 30}
charlie[:gender] = "male" # OK
存在しない要素名にアクセスしたとき
Structは要素の取得時にシンボルと文字列の区別がないの項目でも確認したとおり、Hashでは存在しない要素を取得してもエラーにはならないため、typoに気づきにくくなります。
user = Struct.new(:name, :age, keyword_init: true)
bob = user.new(name: "Bob", age: 20)
p [bob.nema, bob.age] # エラー no member 'nema' in struct (NameError)
charlie = {name: "Charlie", age: 30}
p [charlie[:nema], charlie[:age]] # [nil, 30]
要素の取得方法
Structはドット記法でも要素を取得できます。
user = Struct.new(:name, :age, keyword_init: true)
bob = user.new(name: "Bob", age: 20)
p [bob.name, bob.age] # ["Bob", 20]
p [bob[:name], bob[:age]] # ["Bob", 20]
charlie = {name: "Charlie", age: 30}
p [charlie.name, charlie.age] # ["Charlie", 30]
p [charlie[:name], charlie[:age]] # エラー undefined method `name'
StructとHashの使い分けについての感想
保持する要素数が決まっていないときや、動的に要素を追加することがある場合はHashを使用し、それ以外のときはStructを使うほうが良さそうだなと感じました。
参考文献
Discussion