💎
【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}件取得完了"
注意点とベストプラクティス
注意点
- 無限ループに注意
# BAD: 無限ループ
i = 0
while i < 5
puts i
# i += 1 を忘れている!
end
# GOOD: 終了条件を確実に
i = 0
while i < 5
puts i
i += 1
end
- for文より each を使う
# あまり推奨されない
for i in 1..5
puts i
end
# 推奨
(1..5).each do |i|
puts i
end
- 大量データの処理
# BAD: すべてをメモリに読み込む
lines = File.readlines("huge_file.txt")
lines.each { |line| process(line) }
# GOOD: 1行ずつ処理
File.foreach("huge_file.txt") do |line|
process(line)
end
ベストプラクティス
- 適切なイテレータを選ぶ
# 回数が決まっている場合
5.times { |i| puts i }
# 配列の処理
[1, 2, 3].each { |n| puts n }
# 範囲の処理
(1..100).each { |n| puts n }
- ループの早期終了
# 条件を満たしたら即座に終了
users.each do |user|
if user.admin?
return user
end
end
# または find を使う(より Ruby らしい)
users.find { |user| user.admin? }
- ネストの深さに注意
# 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
解決: プロファイリング(benchmark
gem)で特定し最適化
まとめ
この記事では、ループ(while/until/for/loop)について以下の内容を学びました:
- ループ(while/until/for/loop)の基本概念と重要性
- 基本的な使い方と構文
- 実践的なユースケースとパターン
- 注意点とベストプラクティス
- Ruby 3.4での改善点
ループ(while/until/for/loop)を理解することで、より効率的で保守性の高いRubyコードを書けるようになります。
次のステップ
さらに学習を深めるために:
- 公式ドキュメントで詳細な仕様を確認
- 実際のプロジェクトで実装してみる
- 関連する機能も合わせて学習
- コミュニティのベストプラクティスを参照
Discussion