33.Ruby|ループの種類・使い分け
処理が遅くてトランザクションが中断される問題が頻発していて、リファクタリングをする機会に恵まれた。 この機会にRubyのループ処理について不足している知識を補い整理しておきたい。
forよりもeach
forとeachの違い
まず大きな違いとして、eachはブロック内外でスコープが仕切られているのに対して、 forはブロック内外でスコープが変わらない。
forを原則使わない方が良い理由としては、下記。 for内部はeachにより実装されており(わざわざforを使う理由が薄く)、 新しいスコープが導入されない為、外側からもアクセスできてしまう(無闇にアクセスできない方が良い)。 techracho.bpsinc.jp
スコープを作らないグループ:forやwhile
for
for 変数 in オブジェクト do 繰り返し処理 end
for i in [1, 2, 3] do # forではdoは省略可能 num = i end p num
- forではループ変数(i)もスコープが変わらず、forの外でも参照できる。
for i in [1, 2, 3] end p i
- 出力結果
3
while
a = [1, 2, 3] i = 0 while i < a.size num = a[i] i += 1 end p num
スコープを作るグループ:eachやloop
each
例えばeachだとスコープを作るので、下記のようにスコープ外から呼び出すとエラーとなる。
[1, 2, 3].each do |i| num i end p num
- 出力結果
20201123_loop_test:2:in `block in <main>': undefined method `num' for main:Object (NoMethodError)
eachはブロック内外でスコープが仕切られているが、forはブロック内外でスコープが変わらない。
しかしeachは飽くまでも
eachよりもmap
下記に詳しく書いていくが、mapよりもeachの方が冗長な記述になりやすく、 またループの内外で余計に処理を追加しなくてはいけない。
eachとmapの違い
eachが「元の配列」・mapが「処理後の配列」を返す。
each
list = (1..5).to_a.freeze double = [] # 事前に変数を定義する必要 list.each do |i| double << i * 2 # ループの中でdoubleを組み立てる必要 end p "2倍したリスト:#{double}"
map
list = (1..5).to_a.freeze mapped = list.map do |i| i * 2 # 実行したい処理だけで済む end p "2倍したリスト:#{mapped}"
- 出力結果
"2倍したリスト:[2, 4, 6, 8, 10]"
map処理を簡潔に書きたい場合
例えば下記のような処理があったとして。
user_names = users.map do |user| user.name end
下記のように簡潔に書くこともできる。
この&(アンパサンド)
は、そのメソッドをブロックとして展開することを意味する。
user_names = users.map(&:name)
なおmapメソッドは、Enumerableモジュールに実装されたメソッド。 docs.ruby-lang.org - なおEnumerableモジュールとは、集合を表すクラスに数え上げや検索などのメソッドを提供するパッケージのようなものを指す。
https://tech-camp.in/note/technology/4423/
loop
loopは無限ループを作るのに適している。 意図的にループを終了(break)させない限り、永久に処理を繰り返す。
loop ブロック # 例) loop do 繰り返し処理 終了条件によってループ中断( break ) end
eachよりもfind_each
find_eachは分割してレコードを取得して処理をしてくれるので、eachよりもメモリの消費量を少なく抑えられるケースがある。 デフォルトで1000件ずつの処理をする。
大量のデータを処理する際、DBから取得してきたデータを each のループで回してしまうと、 ループを回す直前にデータを全てメモリ上に展開してしまいます
取得するデータ量が多い場合は、 find_each を使ってDBからデータを分割して取得するようにし、 メモリ上に展開されるデータ数を絞る方がパフォーマンスが良い場合があります
ちなみに、 find_eachは in_batches で取得したデータをeachで返しています
× データ数が多いと一度に全てをメモリ上に展開してしまうためパフォーマンスが悪い
Log.all.each do |log| # 処理を実行 end
◯ DBから分割して取得してくれるので、必要とするメモリの量を抑えながら実行できる
Log.find_each do |log| # 処理を実行 end
each_slice
each_sliceは要素を複数ブロックに分けた上で繰り返し処理をさせたい場合に使う。
(1..10).each_slice(3) {|a| p a} # => [1, 2, 3] # [4, 5, 6] # [7, 8, 9] # [10]