search
LoginSignup
15

posted at

updated at

Organization

【初心者向け】順番に意味がある配列はハッシュに置き換えよう

はじめに:順番に意味のある配列を定義するのは避けよう

プログラミング初心者向けの簡単な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の場合は「順番に意味を持つ配列」に依存するのはなるべく避けた方が良いと思います。

まとめ

というわけで、この記事では順番に意味がある配列を避け、ハッシュ(またはクラス)に置き換える方法を紹介しました。
複数のデータをひとつにまとめたい、と思ったときは、毎回配列を使うのではなく、適切なデータ型を選択できるようになりましょう😉

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
15