webエンジニアの日常

RubyやPython, JSなど、IT関連の記事を書いています

即席クラス。構造化データの表現にはStructを使おう

配列ではなく、キーと値のペアで構成されているデータを扱うには、普通ハッシュを使うかと思います。

それ自体は間違いではありませんが、ハッシュにしてしまうとゲッターメソッドが使えず、オブジェクトかハッシュかを意識しながら扱わなくてはいけません。

要するに、クラスにするまでもない一時的なデータでもオブジェクトのようにゲッターメソッドを使いたいのです。

このような場合にはStructクラスを使うときれいに書くことができます。

Structクラス

例えば以下のようなハッシュを用意したとします。

user= {name: "jon", height: 175, weight: 70}

このデータから身長を取り出すには、

user[:height]

としなければなりません。

また、与えられたデータからBMIを計算する場合は以下のようになります。

user[:bmi] = user[:weight].to_f / ((user[:height] / 100) ** 2)

では、Structクラスを使ってみたいと思います。

UserData = Struct.new(:name, :height, :weight)

user = UserData.new("jon", 175, 70)

user.height
bmi = user.weight.to_f / ((user.height / 100) ** 2)

見た目はオブジェクト指向らしくなりました。

ただ、残念なことにハッシュと異なり新しい属性を追加することはできません。

Structクラスは言わばクラスを作り出すクラスだと言えます。

これは例えではなく、今作ったUserDataはClassクラスのオブジェクトになっており、さらに、Structを親クラスに持ちます。

UserData.class #=> Class
UserData,superclass #=> Struct

これは、以下のようにクラス定義したのと同じです。

class UserData < Struct
  attr_accessor :name, :height, :weight
end

Structのありがたみ

  • メソッド定義

Structによって作られたクラスはゲッターメソッドを提供してくれるほか、とても面白いことができます。

先ほどの例を少し改造します。

UserData = Struct.new(:name, :height, :weight) do
  def bmi
    weight.to_f / ((height / 100) ** 2)
  end
end

user = UserData.new("jon", 175, 70)

bmi = user.bmi

なんと、メソッドの定義ができてしまうのです。

先ほど、新しい属性を追加することはできないと書きましたが、今回の例であればbmiメソッドの追加で対処することができました。

コードの中でメソッド定義が出てくるのはどうかと思うかもしれませんが、もともと構造化データを扱うためのものなので、もしいくつかのメソッドが欲しければクラス定義をしてしまえばいいのです。

  • nil回避

ハッシュは存在しないキーについて参照しようとするとnilを返します。

user = {name: "jon", height: 175, weight: 70}
user[:age] #=> nil

しかし、Structによって作られた構造化データは、存在しない属性にアクセスすると、通常のクラスと同じく例外を出してくれます。

UserData = Struct.new(:name, :height, :weight)

user = UserData.new("jon", 175, 70)

user.age #=> NoMethodError: undefined method `age'

参考

Effective Ruby

Effective Ruby

Ruby on Rails 5アプリケーションプログラミング

Ruby on Rails 5アプリケーションプログラミング