Help us understand the problem. What is going on with this article?

Array#each のブロック内で配列の長さを変更したらどうなる?

More than 5 years have passed since last update.

あまり使うシチュエーションもなさそうだが、興味があったので調べてみた。
Ruby のバージョンは下記の通り。

$ ruby -v
ruby 2.2.2p95 (2015-04-13 revision 5Th0295) [x86_64-darwin14]

実験

a = (0..9).to_a
b = (10..14).to_a
a.each do |i|
  if i == 0
    a.concat(b) # 1 回目のループで末尾に要素追加
  end
  puts i
end
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14

末尾に追加した要素も列挙されるようだ。
最初のほうの要素を削除したらどうなるだろう。

a = (0..9).to_a
a.each do |i|
  if i == 0
    a[0..1] = [] # 1 回目のループで最初の 2 要素を削除
  end
  puts i
end
0
3
4
5
6
7
8
9

1 度目のブロック実行後、a は [2, 3, 4, 5, 6, 7, 8, 9] になっている。
つまり 2 度目のブロックには a[1] が渡されているようだ。

これらの挙動から次のことが予想できる。

  • ブロック実行毎にインデックスをインクリメントして、ブロック実行直前に要素を取り出している。
  • 終了条件のチェックでは毎回、配列の長さを調べ直している。

実装を確認してみよう。

実装の確認

array.c というファイルがあるので、ここに実装がありそう。これかな。

// https://github.com/ruby/ruby/blob/trunk/array.c#L1808
VALUE
rb_ary_each(VALUE ary)
{
    long i;

    RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length);
    for (i=0; i<RARRAY_LEN(ary); i++) {
    rb_yield(RARRAY_AREF(ary, i));
    }
    return ary;
}

// いちおう rb_define_method で Ruby のメソッドと対応してることを確認する。
// https://github.com/ruby/ruby/blob/trunk/array.c#L5810
    rb_define_method(rb_cArray, "each", rb_ary_each, 0);

RETURN_SIZED_ENUMERATOR は block がなければ Enumerator を返すマクロっぽい。
RARRAY_LEN は配列の要素数の取得、RARRAY_AREF はインデックスから要素を取得するマクロだろう (名称と文脈からの推測)。

Ruby で書けばこんな感じだ。
ただし、実際には length や size メソッドを呼んでいるわけではないので、これらのメソッドを上書きしても挙動は変わらない。

class Array
  def each
    return enum_for unless block_given?
    i = 0
    while i < length
      yield self[i]
      i += 1
    end
  end
end

前節で予想した内容と一致した。

labocho
普通の Web プログラマ
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away