🍟

【入門】Rails エンジニアが知っておきたい配列・ハッシュ・Set の違いと使い分け

に公開

はじめに

皆さんは配列・ハッシュ・Set をどのように使い分けていますか?

「なんとなく使い分けている」という方も多いかと思いますが、選び方ひとつでパフォーマンスに大きく差が出ることがあります。

本記事では、基本的な違いから Rails での実践的な使い分けまでを整理しました。

少しでも皆様の参考になりますと幸いです。

注意点

環境

配列(Array)

配列とは

配列とは、複数の値を順番に並べて管理するデータ構造です。
各要素にはインデックス(0始まり)でアクセスします。

fruits = ["りんご", "みかん", "ぶどう"]

fruits[0]  #=> "りんご"
fruits[1]  #=> "みかん"
fruits[2]  #=> "ぶどう"

https://docs.ruby-lang.org/ja/3.3/class/Array.html

配列の特徴

  • 順序がある:要素の並び順が保証される
  • インデックスでアクセス:0, 1, 2, ... の数値で取得する
  • 重複を許容する:同じ値を複数格納できる
  • 任意の型を混在できる:文字列、数値、オブジェクトなどを混ぜて格納できる
# 重複 OK
numbers = [1, 2, 2, 3, 3, 3]

# 型の混在も OK
mixed = ["文字列", 100, true, nil]

配列のよく使うメソッド

要素の追加

fruits = ["りんご", "みかん", "ぶどう"]

fruits.push("もも")  #=> ["りんご", "みかん", "ぶどう", "もも"]
fruits << "いちご"    #=> ["りんご", "みかん", "ぶどう", "もも", "いちご"]

要素数の取得

fruits = ["りんご", "みかん", "ぶどう"]

fruits.length  #=> 3(配列の要素数を返す)
fruits.count   #=> 3(条件付きでも使える)

# count は条件を指定できる
numbers = [1, 2, 2, 2, 3, 3]
numbers.count(2)  #=> 3(値が2の要素数)

繰り返し処理

fruits = ["りんご", "みかん", "ぶどう"]

# 各要素に対して処理を実行
fruits.each do |fruit|
  puts fruit
end

# 各要素を変換して新しい配列を作成
fruits.map { |fruit| "#{fruit}ジュース" }
#=> ["りんごジュース", "みかんジュース", "ぶどうジュース"]

検索・絞り込み

fruits = ["りんご", "みかん", "ぶどう"]

fruits.include?("みかん")  #=> true

# 条件に一致する要素を抽出
fruits.select { |fruit| fruit.include?("ん") }
#=> ["りんご", "みかん"]

並べ替え

numbers = [3, 1, 4, 1, 5]

numbers.sort          #=> [1, 1, 3, 4, 5]
numbers.sort.reverse  #=> [5, 4, 3, 1, 1]

ハッシュ(Hash)

ハッシュとは

ハッシュとは、キーと値のペアでデータを管理するデータ構造です。
各要素にはキーでアクセスします。

user = { name: "佐藤", age: 25, role: "エンジニア" }

user[:name]  #=> "佐藤"
user[:age]   #=> 25
user[:role]  #=> "エンジニア"

https://docs.ruby-lang.org/ja/3.3/class/Hash.html

ハッシュの特徴

  • キーと値のペア:名前付きでデータを管理できる
  • キーは一意:同じキーが重複すると、後から追加した値で上書きされる
  • 順序がある:挿入順が保持される
  • 任意の型をキーにできる:シンボル、文字列、数値など
# キーが重複すると上書き
hash = { a: 1, b: 2, a: 3 }
hash[:a]  #=> 3

# 文字列キーも使える
hash = { "name" => "佐藤", "age" => 25 }

ハッシュのよく使うメソッド

要素の追加・更新

user = { name: "佐藤", age: 25, role: "エンジニア" }

user[:email] = "example@test.com"
user[:age] = 26

キー・値の取得

user = { name: "佐藤", age: 25, role: "エンジニア" }

user.keys    #=> [:name, :age, :role]
user.values  #=> ["佐藤", 25, "エンジニア"]

繰り返し処理

user = { name: "佐藤", age: 25, role: "エンジニア" }

# 各キーと値に対して処理を実行
user.each do |key, value|
  puts "#{key}: #{value}"
end

# 値だけを変換して新しいハッシュを作成
user.transform_values { |value| value.to_s }
#=> { name: "佐藤", age: "25", role: "エンジニア" }

検索・判定

user = { name: "佐藤", age: 25, role: "エンジニア" }

user.key?(:name)          #=> true
user.value?("エンジニア")   #=> true

# 条件に一致する要素を抽出
user.select { |key, value| value.is_a?(String) }
#=> { name: "佐藤", role: "エンジニア" }

マージ

defaults = { role: "一般", active: true }
user = { name: "佐藤", role: "エンジニア" }

# 同じキーがある場合、後のハッシュの値で上書きされる
defaults.merge(user)
#=> { role: "エンジニア", active: true, name: "佐藤" }

Set(集合)

Set とは

Set とは、重複のない要素の集合を管理するデータ構造です。
内部的にはハッシュで実装されており、検索がハッシュと同様に高速です(O(1)については「パフォーマンスの違い」で解説します)。

fruits = Set.new(["りんご", "みかん", "ぶどう"])

# 重複は自動で排除される
fruits.add("りんご")
#=> #<Set: {"りんご", "みかん", "ぶどう"}>

# 高速な検索(O(1))
fruits.include?("みかん")  #=> true

https://docs.ruby-lang.org/ja/3.3/class/Set.html

Set の特徴

  • 重複を許容しない:同じ値を追加しても自動で排除される
  • 検索が高速:内部がハッシュのため include? が O(1) で動作する
  • 順序がある:Ruby の実装上、挿入順が保持される
  • 集合演算ができる:積集合・和集合・差集合などが使える

Set のよく使うメソッド

要素の追加・削除

fruits = Set.new(["りんご", "みかん"])

fruits.add("ぶどう")       #=> #<Set: {"りんご", "みかん", "ぶどう"}>
fruits.delete("みかん")    #=> #<Set: {"りんご", "ぶどう"}>

集合演算

a = Set.new([1, 2, 3])
b = Set.new([2, 3, 4])

a & b   #=> #<Set: {2, 3}>        (積集合:両方に含まれる要素)
a | b   #=> #<Set: {1, 2, 3, 4}>  (和集合:どちらかに含まれる要素)
a - b   #=> #<Set: {1}>           (差集合:a にだけ含まれる要素)

部分集合の判定

a = Set.new([1, 2])
b = Set.new([1, 2, 3, 4])

a.subset?(b)    #=> true(a は b の部分集合)
b.superset?(a)  #=> true(b は a の上位集合)

配列・ハッシュ・Set の違い早見表

項目 配列(Array) ハッシュ(Hash) Set(集合)
アクセス方法 インデックス(数値) キー(シンボル・文字列など) 値そのもの
用途 順序のあるデータの集合 名前付きデータの集合 重複のないデータの集合
重複 値の重複 OK キーの重複 NG(上書き) 値の重複 NG(自動排除)
検索速度 include? は O(n) キー検索は O(1) include? は O(1)
適した場面 一覧・リスト・繰り返し処理 マッピング・集計・設定管理 一意な値の管理・存在チェック

Rails での使い分け

ここからは、Rails の実務でよく見る配列・ハッシュ・Set の使い分けを具体例で紹介します。

配列が適しているケース

1. 一覧データの取得と加工

# pluck で取得したデータは配列として返される
emails = User.where(active: true).pluck(:email)
#=> ["user1@example.com", "user2@example.com", "user3@example.com"]

# 配列のメソッドで加工・繰り返し処理ができる
emails.each do |email|
  NotificationMailer.weekly_report(email).deliver_later
end

2. セレクトボックスの選択肢

# ビューで使う選択肢の一覧
PREFECTURES = ["北海道", "青森県", "岩手県", "宮城県", "秋田県"]

# enum の定義
class Post < ApplicationRecord
  enum :status, [:draft, :published, :archived]
end

3. バッチ処理での一括操作

# ID の一覧を配列で管理し、一括処理
target_ids = [1, 5, 10, 15]
Post.where(id: target_ids).update_all(published: true)

ハッシュが適しているケース

1. 値のマッピング

# ステータスコードと表示名の対応表
# キーで直接アクセスできる(配列では実現しにくい)
STATUS_LABELS = {
  draft: "下書き",
  published: "公開中",
  archived: "アーカイブ"
}

STATUS_LABELS[:published]  #=> "公開中"

2. デフォルト値の管理

# デフォルト設定をハッシュで定義
DEFAULT_CONFIG = {
  theme: "light",
  language: "ja",
  per_page: 20
}

# ユーザーごとの設定で上書き(merge は後のハッシュの値で上書きする)
user_config = { theme: "dark", per_page: 50 }

config = DEFAULT_CONFIG.merge(user_config)
#=> { theme: "dark", language: "ja", per_page: 50 }

3. データの集計

# カテゴリごとの商品数をハッシュで集計
# キーがカテゴリ名、値が件数 → 配列では表現しにくい構造
category_counts = Product.pluck(:category).tally
#=> { "食品" => 15, "飲料" => 8, "日用品" => 12 }

# 特定のカテゴリの件数に直接アクセスできる
category_counts["食品"]  #=> 15

Set が適しているケース

1. 重複排除が必要な一覧の管理

# タグの一覧を重複なしで管理
tags = Set.new
posts.each do |post|
  post.tags.each { |tag| tags.add(tag.name) }
end
# 同じタグ名が何度追加されても重複しない

2. 大量データに対する存在チェック

# 公開済み記事の ID を Set で保持し、高速に判定
published_ids = Set.new(Post.where(status: :published).pluck(:id))

posts.each do |post|
  if published_ids.include?(post.id)
    # 公開済みの処理
  end
end

3. 権限やロールの判定

# ユーザーの権限を Set で管理
admin_permissions = Set.new([:read, :write, :delete, :manage])
editor_permissions = Set.new([:read, :write])

# 権限の差分を確認
admin_permissions - editor_permissions
#=> #<Set: {:delete, :manage}>

# 特定の権限を持っているか判定
editor_permissions.include?(:write)  #=> true

パフォーマンスの違い

配列・ハッシュ・Set では、検索のパフォーマンスに大きな差があります。

O(n) と O(1) とは?

なぜ処理時間が変わるのか?

配列とハッシュ・Set では、データの内部的な保持の仕方が異なります。

配列の場合(O(n)):先頭から順に確認

配列は、データを一列に並べて保持しています。
特定の値を探すには、先頭から順番に 1 つずつ確認していく必要があります。

配列: [りんご] → [みかん] → [ぶどう] → [もも] → [いちご]
       ①確認     ②確認     ③確認     ④確認    ⑤見つかった!

「いちご」を探す → 先頭から5回の確認が必要
→ データが増えるほど、最悪の場合の確認回数も増える

ハッシュ・Set の場合(O(1)):格納場所を計算して直接アクセス

ハッシュ・Set は、値から「格納場所」を計算(ハッシュ関数)して保持しています。
探したい値からも同じ計算で格納場所がわかるため、直接アクセスできます。

"いちご" → ハッシュ関数で格納場所を計算 → 場所[3]に直接アクセス!

格納場所: [0:ぶどう] [1:みかん] [2:もも] [3:いちご] [4:りんご]
                                          ↑ 1回でアクセス!
→ データが何件あっても、計算1回でアクセスできる

実測で比べてみる

以下のコードで、100万件のデータに対する検索速度を比較できます。

n = 1_000_000
array = (1..n).to_a
hash  = (1..n).each_with_object({}) { |i, h| h[i] = true }
set   = Set.new(1..n)

target = n  # 配列の末尾にある要素を検索

Benchmark.bm(8) do |x|
  x.report("Array:") { 1000.times { array.include?(target) } }
  x.report("Hash:")  { 1000.times { hash.key?(target) } }
  x.report("Set:")   { 1000.times { set.include?(target) } }
end

実行結果の例(100万件のデータを 1,000 回検索):

               user     system      total        real
Array:     2.525581   0.000313   2.525894 (  2.525909)
Hash:      0.000050   0.000002   0.000052 (  0.000052)
Set:       0.000049   0.000002   0.000051 (  0.000051)

配列の検索は約 2.5 秒かかっているのに対し、ハッシュ・Set は約 0.00005 秒で完了しています。
約 50,000 倍の差があり、データ量が多く検索頻度が高い場合は大きなパフォーマンス問題になります。

メモリ使用量の違い

検索速度だけでなく、メモリ使用量にも違いがあります。
ハッシュ・Set は高速な検索を実現するために、内部でハッシュテーブルという仕組みを保持しているため、配列より多くのメモリを消費します。

以下のコードでメモリ使用量を確認できます。

n = 1_000_000
array = (1..n).to_a
hash  = (1..n).each_with_object({}) { |i, h| h[i] = true }
set   = Set.new(1..n)

puts "Array: #{ObjectSpace.memsize_of(array)} bytes"
puts "Hash:  #{ObjectSpace.memsize_of(hash)} bytes"
puts "Set:   #{ObjectSpace.memsize_of(set.instance_variable_get(:@hash))} bytes"

実行結果の例(100万件の整数データ):

Array: 11636312 bytes
Hash:  33554592 bytes
Set:   33554592 bytes

迷ったときの判断基準

最後に、配列・ハッシュ・Set のどれを使うか迷ったときの判断基準をまとめます。

配列を選ぶとき:

  • データの順序が重要
  • 繰り返し処理がメインの用途
  • 要素に名前が不要

ハッシュを選ぶとき:

  • キーで直接アクセスしたい
  • データに名前を付けたい
  • キーと値のマッピングで高速にアクセスしたい

Set を選ぶとき:

  • 重複のない値の集合を扱いたい
  • 「含まれるかどうか」の判定を高速に行いたい
  • 集合演算(積集合・和集合・差集合)を使いたい

まとめ

配列・ハッシュ・Set の違いと使い分けについてまとめてみました。

  • 配列は「順番のあるデータの集まり」に適している
  • ハッシュは「名前付きデータの管理」や「高速な検索」に適している
  • Set は「重複のない値の集合」や「高速な存在チェック」に適している
  • 検索頻度が高い場合は、配列よりもハッシュや Set を使うことでパフォーマンスが向上する

適切に配列・ハッシュ・Set を使い分けることでパフォーマンスを向上させていきましょう。

最後までお読みいただき、ありがとうございました。

GitHubで編集を提案
株式会社L&E Group

Discussion