はじめに:順番に意味のある配列を定義するのは避けよう
プログラミング初心者向けの簡単なTipsです。
何か複数のデータをまとめてひとつの変数に保持したい場合には配列が使えます。
しかし「複数のデータ」とひとことに言っても、配列にまとめて良いデータといけないデータがあります。
たとえば以下のようなデータは配列にまとめるのは望ましくありません。
user_data = [
170, # 身長
65, # 体重
"AB" # 血液型
]
上のような配列は順番が意味を持ってしまいます。すなわち、
- indexが0のデータは身長(0以外のindexを指定してはダメ)
- indexが1のデータは体重(1以外のindexを指定してはダメ)
- indexが2のデータは血液型(2以外のindexを指定してはダメ)
という暗黙のルールが生まれてしまうためです。
この配列を適切に扱うためには、index(添え字)を間違いなく指定する必要があります。
それゆえ、うっかり以下のようなコードを書いてしまうとバグになってしまいます。
# あっ、index=1は体重ですよ!!
puts "私の身長は#{user_data[1]}cmです"
#=> 私の身長は65cmです
プログラムが小さいうち(100行未満)はこういった配列を使っていてもバグを出さずにコードを書けるかもしれませんが、業務で扱うような巨大で複雑なプログラムになると、「暗黙のルール」が仇(あだ)になってバグの原因になります。
正しい配列かどうかをチェックする方法
「ループ処理でまったく同じ処理を書けるか?(indexによって条件分岐が発生しないか?)」、数値の配列であれば「単純にデータを合計(sum)できるか?(もしくはその結果に意味があるか?)」といった点を自問自答してみましょう。
その答えがNoなら、配列を使うのは適切でない可能性が高いです。
user_data.each_with_index do |value, index|
# ループ処理してるのにindexに応じて条件分岐するのは何かおかしい
case index
when 0
puts "身長 = #{value}cm"
when 1
puts "体重 = #{value}kg"
when 2
puts "血液型 = #{value}"
end
end
#=> 身長 = 170cm
# 体重 = 65kg
# 血液型 = AB
# 単純にsumできないのは何かおかしい(配列に異なるデータ型が混在している)
user_data.sum
#=> `+': String can't be coerced into Integer (TypeError)
# 仮に最初の2要素だけをsumしても、出てきた数値に意味がない(235っていったい何??)
user_data[0..1].sum
#=> 235
一方、クラス全員のテストの得点などは配列にまとめても構いません。
# クラス全員のテストの得点を配列にまとめるのは問題なし
points = [90, 43, 100]
# ループ処理でまったく同じ処理を書けるか? = YES
points.each do |point|
puts "得点 = #{point}点"
end
#=> 得点 = 90点
# 得点 = 43点
# 得点 = 100点
# 単純にデータを合計(sum)できるか? = YES
points.sum #=> 233
では、「順番に意味のある、おかしな配列」はどう書き換えればいいのでしょうか?
修正方法:代わりにハッシュを使おう
こういうケースではハッシュを使うのが良いです。
冒頭で示したuser_data
はハッシュを使って次のように書き換えることができます。
user_data = {
height: 170,
weight: 65,
blood_type: "AB"
}
こうすればindexではなく、キーで値を参照できるようになるため、不具合が発生する確率が減りますし、可読性も良くなります。
# ハッシュであればキーで指定できるので、:height = 身長ということが見て明らか
puts "私の身長は#{user_data[:height]}cmです"
#=> 私の身長は170cmです
応用:クラスやStructを使うのもアリ
他にもクラスやStructを使う方法もあります。以下はクラスを定義するコード例です。
class UserData
attr_reader :height, :weight, :blood_type
def initialize(height, weight, blood_type)
@height = height
@weight = weight
@blood_type = blood_type
end
end
user_data = UserData.new(170, 65, "AB")
puts "私の身長は#{user_data.height}cmです"
#=> 私の身長は170cmです
ハッシュの場合、うっかりキーをtypoするとnil
が返りますが、クラスであればエラーになるのでミスに気づきやすくなります。
# ハッシュでキーをtypoするとnilが返るので間違いに気づきにくい
puts "私の身長は#{user_data[:heihgt]}cmです"
#=> 私の身長はcmです
# クラスであればtypoするとエラーが発生するのですぐ気づける
puts "私の身長は#{user_data.heihgt}cmです"
#=> undefined method `heihgt' for #<UserData:...> (NoMethodError)
# Did you mean? height
「単純なデータの入れ物」としての用途しかないのであれば、クラスの代わりにStructを使う(Structでシンプルにクラス定義する)方法もあります。
UserData = Struct.new(:height, :weight, :blood_type)
user_data = UserData.new(170, 65, "AB")
puts "私の身長は#{user_data.height}cmです"
#=> 私の身長は170cmです
typoしたときにエラーが発生する点もクラスと同じです。
puts "私の身長は#{user_data.heihgt}cmです"
#=> undefined method `heihgt' for #<struct UserData ...> (NoMethodError)
# Did you mean? height
順番に意味がある配列=タプル?
最初に示したような「順番に意味を持つ配列」はタプルと呼ばれるデータ型に近いもの、と考えることもできます。
# 順番に意味がある配列はタプルに近い
user_data = [
170, # 身長
65, # 体重
"AB" # 血液型
]
たとえばPythonはデータ型としてタプルをサポートしています。
# Pythonでタプルを使うコード例
user_data = (
170, # 身長
65, # 体重
"AB" # 血液型
)
print(user_data[0]) #=> 170
print(user_data[1]) #=> 65
print(user_data[2]) #=> AB
ただし、タプルはイミュータブルなので、あとから要素を変更したり追加したりすることができません。
# Pythonのタプルはデータを変更できない
user_data[0] = 180
#=> TypeError: 'tuple' object does not support item assignment
Rubyは配列をタプルっぽく使うことはできても、イミュータブルではない(要素の変更や追加ができてしまう)ので、タプルの完全な代替品として使うことはできません。そういう理由もあって、Rubyの場合は「順番に意味を持つ配列」に依存するのはなるべく避けた方が良いと思います。
まとめ
というわけで、この記事では順番に意味がある配列を避け、ハッシュ(またはクラス)に置き換える方法を紹介しました。
複数のデータをひとつにまとめたい、と思ったときは、毎回配列を使うのではなく、適切なデータ型を選択できるようになりましょう😉