💎

【Ruby 4日目】基本文法 - ループ(while/until/for/loop)

に公開

はじめに

Rubyのループ(while/until/for/loop)について、Ruby 3.4の仕様に基づいて詳しく解説します。基本文法の理解は、Rubyプログラミングの基礎となる重要な要素です。

この記事では、基本的な概念から実践的な使い方まで、具体的なコード例を交えて説明します。

基本概念

ループ(while/until/for/loop)の主な特徴:

  • Rubyの動的な特性を活かした柔軟な記法
  • 直感的で読みやすい構文設計
  • 強力な表現力と簡潔性の両立

Rubyではループ(while/until/for/loop)を使用することで、より効率的で読みやすいコードを書くことができます。

基本的な使い方

while文

# while文の基本
i = 0
while i < 5
  puts i
  i += 1
end
# 出力: 0, 1, 2, 3, 4

# 後置while(修飾子形式)
i = 0
begin
  puts i
  i += 1
end while i < 5

# 無限ループからbreak
i = 0
while true
  puts i
  i += 1
  break if i >= 5
end

until文(whileの逆)

# until文の基本
i = 0
until i >= 5
  puts i
  i += 1
end
# 出力: 0, 1, 2, 3, 4

# 後置until
i = 0
begin
  puts i
  i += 1
end until i >= 5

for文

# for文(配列)
for num in [1, 2, 3, 4, 5]
  puts num
end

# for文(範囲)
for i in 1..5
  puts i
end

# ただし、Rubyでは each の方が推奨される
[1, 2, 3, 4, 5].each do |num|
  puts num
end

loop文

# 無限ループ
i = 0
loop do
  puts i
  i += 1
  break if i >= 5
end

# loopはKernelモジュールのメソッド
# while true よりも意図が明確

イテレータ(推奨)

# times
5.times do |i|
  puts i  # 0から4まで
end

# upto
1.upto(5) do |i|
  puts i  # 1から5まで
end

# downto
5.downto(1) do |i|
  puts i  # 5から1まで
end

# step
0.step(10, 2) do |i|
  puts i  # 0, 2, 4, 6, 8, 10
end

# each(配列)
[1, 2, 3].each do |num|
  puts num
end

# each_with_index
["a", "b", "c"].each_with_index do |char, index|
  puts "#{index}: #{char}"
end

break、next、redo

# break: ループを抜ける
5.times do |i|
  break if i == 3
  puts i
end
# 出力: 0, 1, 2

# next: 次の繰り返しへ(continueに相当)
5.times do |i|
  next if i == 2
  puts i
end
# 出力: 0, 1, 3, 4

# redo: 同じ繰り返しをやり直す
i = 0
5.times do
  puts i
  i += 1
  redo if i == 2 && rand < 0.5  # 50%の確率でやり直し
end

基本的な使い方を理解したら、次は実践的なユースケースを見ていきましょう。

よくあるユースケース

ケース1: 配列の処理

実務でよく使われる配列操作パターンです。

# 配列の要素を処理
users = [
  { name: "Alice", age: 30 },
  { name: "Bob", age: 25 },
  { name: "Charlie", age: 35 }
]

# each で処理
users.each do |user|
  puts "#{user[:name]}さんは#{user[:age]}歳です"
end

# select で絞り込み
adults = users.select { |user| user[:age] >= 30 }
puts "30歳以上: #{adults.map { |u| u[:name] }.join(', ')}"

# map で変換
names = users.map { |user| user[:name] }
puts "名前一覧: #{names.join(', ')}"

# each_with_index でインデックス付き処理
users.each_with_index do |user, index|
  puts "#{index + 1}. #{user[:name]}"
end

ケース2: ファイル処理

ファイルを1行ずつ読み込むパターンです。

# ファイルを1行ずつ処理
def process_log_file(filename)
  error_count = 0

  File.open(filename, "r") do |file|
    file.each_line.with_index(1) do |line, line_number|
      if line.include?("ERROR")
        puts "#{line_number}行目にエラー: #{line.strip}"
        error_count += 1
      end
    end
  end

  puts "エラー総数: #{error_count}"
rescue Errno::ENOENT
  puts "ファイルが見つかりません: #{filename}"
end

# 使用例(仮想的な例)
# process_log_file("app.log")

ケース3: リトライ処理

エラー時に再試行するパターンです。

def fetch_data_with_retry(max_retries = 3)
  retries = 0

  begin
    # 仮想的なAPI呼び出し
    puts "データ取得中... (試行 #{retries + 1})"

    # ランダムに失敗をシミュレート
    raise "接続エラー" if rand < 0.5

    puts "データ取得成功!"
    return { status: "success", data: "sample data" }

  rescue => e
    retries += 1

    if retries < max_retries
      puts "エラー: #{e.message}. #{retries}秒後に再試行..."
      sleep retries
      retry
    else
      puts "最大試行回数に達しました"
      return { status: "error", message: e.message }
    end
  end
end

# 使用例
result = fetch_data_with_retry(3)
puts result

ケース4: バッチ処理

大量データを分割処理するパターンです。

def process_in_batches(items, batch_size = 100)
  total = items.size
  processed = 0

  items.each_slice(batch_size).with_index do |batch, batch_number|
    puts "バッチ #{batch_number + 1} 処理中..."

    batch.each do |item|
      # 個別の処理
      process_item(item)
      processed += 1

      # 進捗表示
      if processed % 50 == 0
        progress = (processed.to_f / total * 100).round(1)
        puts "進捗: #{processed}/#{total} (#{progress}%)"
      end
    end

    # バッチ間で少し待機
    sleep 0.1
  end

  puts "処理完了: #{processed}件"
end

def process_item(item)
  # 仮想的な処理
  # データベース保存、API呼び出しなど
end

# 使用例
items = (1..500).to_a
process_in_batches(items, 100)

ケース5: ページネーション処理

APIのページングを処理するパターンです。

def fetch_all_pages
  page = 1
  all_results = []

  loop do
    puts "ページ #{page} を取得中..."

    # 仮想的なAPI呼び出し
    results = fetch_page(page)

    break if results.empty?

    all_results.concat(results)
    puts "#{results.size}件取得(累計: #{all_results.size}件)"

    page += 1

    # 無限ループ防止
    break if page > 100
  end

  all_results
end

def fetch_page(page)
  # 仮想的なAPIレスポンス
  return [] if page > 5  # 5ページで終了

  (1..10).map { |i| { id: (page - 1) * 10 + i, name: "Item #{(page - 1) * 10 + i}" } }
end

# 使用例
all_data = fetch_all_pages
puts "全#{all_data.size}件取得完了"

注意点とベストプラクティス

注意点

  1. 無限ループに注意
# BAD: 無限ループ
i = 0
while i < 5
  puts i
  # i += 1 を忘れている!
end

# GOOD: 終了条件を確実に
i = 0
while i < 5
  puts i
  i += 1
end
  1. for文より each を使う
# あまり推奨されない
for i in 1..5
  puts i
end

# 推奨
(1..5).each do |i|
  puts i
end
  1. 大量データの処理
# BAD: すべてをメモリに読み込む
lines = File.readlines("huge_file.txt")
lines.each { |line| process(line) }

# GOOD: 1行ずつ処理
File.foreach("huge_file.txt") do |line|
  process(line)
end

ベストプラクティス

  1. 適切なイテレータを選ぶ
# 回数が決まっている場合
5.times { |i| puts i }

# 配列の処理
[1, 2, 3].each { |n| puts n }

# 範囲の処理
(1..100).each { |n| puts n }
  1. ループの早期終了
# 条件を満たしたら即座に終了
users.each do |user|
  if user.admin?
    return user
  end
end

# または find を使う(より Ruby らしい)
users.find { |user| user.admin? }
  1. ネストの深さに注意
# BAD: 深いネスト
users.each do |user|
  user.posts.each do |post|
    post.comments.each do |comment|
      # 処理
    end
  end
end

# GOOD: メソッドに分割
users.each do |user|
  process_user_content(user)
end

def process_user_content(user)
  user.posts.each do |post|
    process_post_comments(post)
  end
end

Ruby 3.4での改善点

Ruby 3.4では、イテレータのパフォーマンスが向上しています:

  • YJIT最適化: ループ処理が最大30%高速化
  • メモリ効率: each_sliceなどのバッチ処理が効率化
  • エラーメッセージ: より分かりやすいスタックトレース

実践的なサンプルコード

実際のプロジェクトで使える、より実践的な例です。

# CSVファイル処理システム
require 'csv'

class DataImporter
  def initialize(filepath)
    @filepath = filepath
    @successful = 0
    @failed = 0
    @errors = []
  end

  def import
    puts "インポート開始: #{@filepath}"

    CSV.foreach(@filepath, headers: true).with_index(2) do |row, line_number|
      begin
        process_row(row)
        @successful += 1

        # 進捗表示(100件ごと)
        if @successful % 100 == 0
          puts "#{@successful}件処理完了..."
        end

      rescue => e
        @failed += 1
        @errors << { line: line_number, error: e.message, data: row.to_h }
        puts "#{line_number}行目でエラー: #{e.message}"
      end
    end

    print_summary
  end

  private

  def process_row(row)
    # データの検証
    validate_row(row)

    # データベースへの保存(仮想的)
    save_to_database(row.to_h)
  end

  def validate_row(row)
    raise "名前が空です" if row['name'].nil? || row['name'].empty?
    raise "年齢が不正です" unless row['age'].to_i.between?(0, 150)
  end

  def save_to_database(data)
    # 実際のDB保存処理
    # User.create!(data)
  end

  def print_summary
    puts "\n" + "=" * 50
    puts "インポート完了"
    puts "成功: #{@successful}件"
    puts "失敗: #{@failed}件"

    if @errors.any?
      puts "\nエラー詳細:"
      @errors.first(5).each do |error|
        puts "  行#{error[:line]}: #{error[:error]}"
      end
      puts "  ... 他#{@errors.size - 5}件" if @errors.size > 5
    end
  end
end

# 使用例
# importer = DataImporter.new("users.csv")
# importer.import

デバッグとトラブルシューティング

よくある問題と解決方法:

問題1: 無限ループ

原因: 終了条件が満たされない

# BAD
i = 0
while i < 10
  puts i
  # i += 1 を忘れている
end

# GOOD: デバッグ出力を追加
i = 0
while i < 10
  puts "i = #{i}"  # 現在の値を確認
  i += 1
end

解決: ループカウンタを必ず更新する、またはtimesを使う

問題2: ループ内のスコープ問題

原因: ブロック内外でのスコープの違い

# ループ外で変数を初期化
total = 0
[1, 2, 3].each do |num|
  total += num  # ループ外の変数にアクセス
end
puts total  # => 6

問題3: パフォーマンス問題

原因: N+1クエリ問題、非効率な処理

# BAD: N+1問題
users.each do |user|
  user.posts.each do |post|  # 毎回DBクエリ
    puts post.title
  end
end

# GOOD: 一括読み込み
users.includes(:posts).each do |user|
  user.posts.each do |post|
    puts post.title
  end
end

解決: プロファイリング(benchmarkgem)で特定し最適化

まとめ

この記事では、ループ(while/until/for/loop)について以下の内容を学びました:

  • ループ(while/until/for/loop)の基本概念と重要性
  • 基本的な使い方と構文
  • 実践的なユースケースとパターン
  • 注意点とベストプラクティス
  • Ruby 3.4での改善点

ループ(while/until/for/loop)を理解することで、より効率的で保守性の高いRubyコードを書けるようになります。

次のステップ

さらに学習を深めるために:

  • 公式ドキュメントで詳細な仕様を確認
  • 実際のプロジェクトで実装してみる
  • 関連する機能も合わせて学習
  • コミュニティのベストプラクティスを参照

参考資料

Discussion